diff --git a/frontend/components/containers/interactive-session.tsx b/frontend/components/containers/interactive-session.tsx
index 02d951d..811d457 100644
--- a/frontend/components/containers/interactive-session.tsx
+++ b/frontend/components/containers/interactive-session.tsx
@@ -52,10 +52,9 @@ const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
) : (
);
-
- default:
- throw new Error("Unknown interactive session type");
}
+
+ return null;
};
export default InteractiveSession;
diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx
index ea9a991..a65f521 100644
--- a/frontend/components/ui/button.tsx
+++ b/frontend/components/ui/button.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { GetProps, Button as BaseButton, Spinner } from "tamagui";
+import Icons from "./icons";
type ButtonProps = GetProps & {
isDisabled?: boolean;
@@ -16,4 +17,14 @@ const Button = ({ icon, isLoading, isDisabled, ...props }: ButtonProps) => {
);
};
+export const BackButton = (props: GetProps) => (
+ }
+ mr={6}
+ {...props}
+ />
+);
+
export default Button;
diff --git a/frontend/components/ui/grid-view.tsx b/frontend/components/ui/grid-view.tsx
index 6cc9ed3..a6a5815 100644
--- a/frontend/components/ui/grid-view.tsx
+++ b/frontend/components/ui/grid-view.tsx
@@ -3,9 +3,32 @@ import { GetProps, ScrollView, View, ViewStyle } from "tamagui";
type GridItem = { key: string };
-type GridViewProps = GetProps & {
+type GridViewProps = GetProps &
+ GridLayoutProps;
+
+const GridView = ({
+ data,
+ renderItem,
+ columns,
+ gap,
+ ...props
+}: GridViewProps) => {
+ return (
+
+
+
+ );
+};
+
+type GridLayoutProps = GetProps & {
data?: T[] | null;
renderItem: (item: T, index: number) => React.ReactNode;
+ gap?: string | number;
columns: {
xs?: number;
sm?: number;
@@ -15,10 +38,10 @@ type GridViewProps = GetProps & {
};
};
-const GridView = ({
+export const GridLayout = ({
+ columns,
data,
renderItem,
- columns,
gap,
...props
}: GridViewProps) => {
@@ -33,20 +56,13 @@ const GridView = ({
}, [columns]);
return (
-
+
{data?.map((item, idx) => (
{renderItem(item, idx)}
))}
-
+
);
};
diff --git a/frontend/components/ui/pressable.tsx b/frontend/components/ui/pressable.tsx
index 5395a11..be74952 100644
--- a/frontend/components/ui/pressable.tsx
+++ b/frontend/components/ui/pressable.tsx
@@ -63,6 +63,7 @@ export const MultiTapPressable = ({
}}
numberOfTaps={numberOfTaps}
ref={tapRef}
+ maxDelayMs={120}
>
diff --git a/frontend/hooks/useQueryParams.ts b/frontend/hooks/useQueryParams.ts
new file mode 100644
index 0000000..40706cb
--- /dev/null
+++ b/frontend/hooks/useQueryParams.ts
@@ -0,0 +1,38 @@
+import { router, useLocalSearchParams } from "expo-router";
+import { useCallback, useMemo, useState } from "react";
+
+export const useQueryParams = () => {
+ const params = useLocalSearchParams() as T;
+ const [history, setHistory] = useState([params]);
+
+ const push = useCallback((params: T) => {
+ setHistory((prev) => [...prev, params]);
+ router.setParams(params);
+ }, []);
+
+ const replace = useCallback((params: T) => {
+ setHistory([params]);
+ router.setParams(params);
+ }, []);
+
+ const canGoBack = useMemo(() => history.length > 1, [history]);
+
+ const goBack = useCallback(() => {
+ if (!canGoBack) {
+ return false;
+ }
+
+ const historyList = [...history];
+ historyList.pop();
+ const prev = historyList[historyList.length - 1];
+ if (!prev) {
+ return false;
+ }
+
+ setHistory(historyList);
+ router.setParams(prev);
+ return true;
+ }, [history, canGoBack]);
+
+ return { params, replace, push, goBack, canGoBack };
+};
diff --git a/frontend/pages/auth/login.tsx b/frontend/pages/auth/login.tsx
index 2796985..1b10da8 100644
--- a/frontend/pages/auth/login.tsx
+++ b/frontend/pages/auth/login.tsx
@@ -58,6 +58,7 @@ export default function LoginPage() {
name="username"
autoCapitalize="none"
onSubmitEditing={onSubmit}
+ autoFocus
/>
diff --git a/frontend/pages/hosts/components/form.tsx b/frontend/pages/hosts/components/form.tsx
index 04b5172..f063d77 100644
--- a/frontend/pages/hosts/components/form.tsx
+++ b/frontend/pages/hosts/components/form.tsx
@@ -47,22 +47,30 @@ const HostForm = () => {
-
-
-
-
-
-
-
+ {type !== "group" && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
{type === "ssh" ? (
diff --git a/frontend/pages/hosts/components/host-item.tsx b/frontend/pages/hosts/components/host-item.tsx
index 4a406f8..800fb4b 100644
--- a/frontend/pages/hosts/components/host-item.tsx
+++ b/frontend/pages/hosts/components/host-item.tsx
@@ -6,12 +6,19 @@ import OSIcons from "@/components/ui/os-icons";
type HostItemProps = {
host: any;
+ selected?: boolean;
onMultiTap: () => void;
- onTap: () => void;
+ onTap?: () => void;
onEdit?: (() => void) | null;
};
-const HostItem = ({ host, onMultiTap, onTap, onEdit }: HostItemProps) => {
+const HostItem = ({
+ host,
+ selected,
+ onMultiTap,
+ onTap,
+ onEdit,
+}: HostItemProps) => {
return (
{
onMultiTap={onMultiTap}
onTap={onTap}
>
-
+
-
+ {host.type === "group" ? (
+
+ ) : (
+
+ )}
{host.label}
diff --git a/frontend/pages/hosts/components/host-list.tsx b/frontend/pages/hosts/components/host-list.tsx
index 98d0f3d..ddb61bd 100644
--- a/frontend/pages/hosts/components/host-list.tsx
+++ b/frontend/pages/hosts/components/host-list.tsx
@@ -1,26 +1,36 @@
-import { View, Text, Spinner } from "tamagui";
+import { View, Text, Spinner, ScrollView } from "tamagui";
import React, { useMemo, useState } from "react";
import { useNavigation } from "expo-router";
import SearchInput from "@/components/ui/search-input";
import { useTermSession } from "@/stores/terminal-sessions";
import { hostFormModal } from "./form";
-import GridView from "@/components/ui/grid-view";
+import { GridLayout } from "@/components/ui/grid-view";
import HostItem from "./host-item";
import { useHosts } from "../hooks/query";
type HostsListProps = {
allowEdit?: boolean;
+ parentId?: string | null;
+ onParentIdChange?: (id: string | null) => void;
+ selected?: string[];
+ onSelectedChange?: (ids: string[]) => void;
};
-const HostList = ({ allowEdit = true }: HostsListProps) => {
+const HostList = ({
+ allowEdit = true,
+ parentId,
+ onParentIdChange,
+ selected = [],
+ onSelectedChange,
+}: HostsListProps) => {
const openSession = useTermSession((i) => i.push);
const navigation = useNavigation();
const [search, setSearch] = useState("");
- const hosts = useHosts();
+ const { data, isLoading } = useHosts({ parentId });
const hostsList = useMemo(() => {
- let items = hosts.data || [];
+ let items = data || [];
if (search) {
items = items.filter((item: any) => {
@@ -33,7 +43,25 @@ const HostList = ({ allowEdit = true }: HostsListProps) => {
}
return items.map((i: any) => ({ ...i, key: i.id }));
- }, [hosts.data, search]);
+ }, [data, search]);
+
+ const groups = useMemo(
+ () => hostsList.filter((i: any) => i.type === "group"),
+ [hostsList]
+ );
+
+ const hosts = useMemo(
+ () => hostsList.filter((i: any) => i.type !== "group"),
+ [hostsList]
+ );
+
+ const onSelect = (host: any) => {
+ if (selected.includes(host.id)) {
+ onSelectedChange?.(selected.filter((i) => i !== host.id));
+ } else {
+ onSelectedChange?.([...selected, host.id]);
+ }
+ };
const onEdit = (host: any) => {
if (!allowEdit) return;
@@ -68,29 +96,76 @@ const HostList = ({ allowEdit = true }: HostsListProps) => {
/>
- {hosts.isLoading ? (
+ {isLoading ? (
Loading...
) : (
- (
- {}}
- onMultiTap={() => onOpenTerminal(host)}
- onEdit={allowEdit ? () => onEdit(host) : null}
- />
+
+ {groups.length > 0 && (
+ <>
+ Groups
+ onParentIdChange?.(group.id)}
+ onEdit={allowEdit ? onEdit : undefined}
+ />
+ >
)}
- />
+
+ Hosts
+ {!hosts.length && (
+
+ No hosts found
+
+ )}
+
+
+
)}
>
);
};
+type ItemListProps = {
+ data?: any[];
+ selected?: string[];
+ onTap?: (host: any) => void;
+ onMultiTap?: (host: any) => void;
+ onEdit?: (host: any) => void;
+};
+
+const ItemList = ({
+ data,
+ selected,
+ onTap,
+ onMultiTap,
+ onEdit,
+}: ItemListProps) => (
+ (
+ onTap?.(host) : undefined}
+ onMultiTap={() => onMultiTap?.(host)}
+ onEdit={onEdit ? () => onEdit?.(host) : undefined}
+ />
+ )}
+ />
+);
+
export default React.memo(HostList);
diff --git a/frontend/pages/hosts/hooks/query.ts b/frontend/pages/hosts/hooks/query.ts
index 2acfd5c..19da9b7 100644
--- a/frontend/pages/hosts/hooks/query.ts
+++ b/frontend/pages/hosts/hooks/query.ts
@@ -5,12 +5,13 @@ import { useMemo } from "react";
import { useKeychains } from "@/pages/keychains/hooks/query";
import queryClient from "@/lib/queryClient";
import { useTeamId } from "@/stores/auth";
+import { MoveHostPayload } from "../schema/query";
-export const useHosts = () => {
+export const useHosts = (params: any = {}) => {
const teamId = useTeamId();
return useQuery({
- queryKey: ["hosts", teamId],
- queryFn: () => api("/hosts", { params: { teamId } }),
+ queryKey: ["hosts", teamId, params],
+ queryFn: () => api("/hosts", { params: { teamId, ...params } }),
select: (i) => i.rows,
});
};
@@ -40,7 +41,26 @@ export const useSaveHost = () => {
? api(`/hosts/${body.id}`, { method: "PUT", body })
: api(`/hosts`, { method: "POST", body });
},
- onError: (e) => console.error(e),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["hosts"] });
+ },
+ });
+};
+
+export const useMoveHost = () => {
+ const teamId = useTeamId();
+
+ return useMutation({
+ mutationFn: (data: MoveHostPayload) => {
+ const hostId = Array.isArray(data.hostId)
+ ? data.hostId.join(",")
+ : data.hostId;
+
+ return api("/hosts/move", {
+ method: "POST",
+ body: { teamId, parentId: data.parentId, hostId },
+ });
+ },
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["hosts"] });
},
diff --git a/frontend/pages/hosts/page.tsx b/frontend/pages/hosts/page.tsx
index 1c3a3ea..35be4c8 100644
--- a/frontend/pages/hosts/page.tsx
+++ b/frontend/pages/hosts/page.tsx
@@ -1,5 +1,5 @@
-import { Button } from "tamagui";
-import React from "react";
+import { Button, GetProps, useMedia } from "tamagui";
+import React, { useMemo, useState } from "react";
import Drawer from "expo-router/drawer";
import HostList from "./components/host-list";
import HostForm, { hostFormModal } from "./components/form";
@@ -8,36 +8,152 @@ import { initialValues } from "./schema/form";
import KeyForm from "../keychains/components/form";
import { useUser } from "@/hooks/useUser";
import { useTeamId } from "@/stores/auth";
+import { useMoveHost } from "./hooks/query";
+import { useQueryParams } from "@/hooks/useQueryParams";
+import { BackButton } from "@/components/ui/button";
+
+type Params = {
+ parentId?: string | null;
+};
export default function HostsPage() {
const teamId = useTeamId();
const user = useUser();
+ const [selected, setSelected] = useState([]);
+ const queryParams = useQueryParams();
+ const parentId = queryParams.params?.parentId;
+ const media = useMedia();
+
+ const setParentId = (id: string | null) => {
+ queryParams.push({ parentId: id || "" });
+ };
+
+ const onGoBack = () => {
+ if (!queryParams.goBack()) {
+ queryParams.replace({ parentId: "" });
+ }
+ };
+
+ const actions = useMemo(() => {
+ if (selected?.length > 0) {
+ return (
+ setSelected([])}
+ />
+ );
+ }
+
+ if (!teamId || user?.teamCanWrite(teamId)) {
+ return (
+ hostFormModal.onOpen({ ...initialValues, parentId })}
+ />
+ );
+ }
+
+ return null;
+ }, [teamId, user, parentId]);
return (
<>
- : undefined,
+ headerLeft: parentId
+ ? () =>
+ : media.gtXs
+ ? () => null
+ : undefined,
+ headerTitle:
+ selected.length > 0 ? `Selected ${selected.length} hosts` : "Hosts",
+ headerRight: () => actions,
}}
/>
-
+
+
>
);
}
-const AddButton = () => (
+const AddButton = (props: GetProps) => (
}
- onPress={() => hostFormModal.onOpen(initialValues)}
- $gtSm={{ mr: "$3" }}
+ $gtSm={{ mr: "$2" }}
+ {...props}
>
New
);
+
+type HostsActionsProps = {
+ selected: string[];
+ parentId?: string | null;
+ onClear: () => void;
+};
+
+const actionMode = {
+ CUT: 1,
+};
+
+const HostsActions = ({
+ selected,
+ parentId = null,
+ onClear,
+}: HostsActionsProps) => {
+ const [curMode, setCurMode] = useState(0);
+ const move = useMoveHost();
+
+ const onReset = () => {
+ setCurMode(0);
+ onClear();
+ };
+
+ const onMoveAction = () => {
+ move.mutate({ parentId, hostId: selected }, { onSuccess: onReset });
+ };
+
+ return (
+ <>
+ {curMode === actionMode.CUT ? (
+ }
+ bg="$colorTransparent"
+ onPress={onMoveAction}
+ />
+ ) : (
+ }
+ bg="$colorTransparent"
+ onPress={() => setCurMode(actionMode.CUT)}
+ />
+ )}
+ {/* }
+ bg="$colorTransparent"
+ /> */}
+ }
+ bg="$colorTransparent"
+ onPress={onReset}
+ mr="$2"
+ />
+ >
+ );
+};
diff --git a/frontend/pages/hosts/schema/form.ts b/frontend/pages/hosts/schema/form.ts
index f677242..121d9c8 100644
--- a/frontend/pages/hosts/schema/form.ts
+++ b/frontend/pages/hosts/schema/form.ts
@@ -8,6 +8,10 @@ export const baseFormSchema = z.object({
parentId: z.string().ulid().nullish(),
});
+const groupSchema = baseFormSchema.merge(
+ z.object({ type: z.literal("group") })
+);
+
const hostSchema = baseFormSchema.merge(
z.object({
host: hostnameShape(),
@@ -52,7 +56,7 @@ const incusSchema = hostSchema.merge(
export const formSchema = z.discriminatedUnion(
"type",
- [sshSchema, pveSchema, incusSchema],
+ [groupSchema, sshSchema, pveSchema, incusSchema],
{ errorMap: () => ({ message: "Invalid host type" }) }
);
@@ -67,6 +71,7 @@ export const initialValues: FormSchema = {
};
export const typeOptions: SelectItem[] = [
+ { label: "Group", value: "group" },
{ label: "SSH", value: "ssh" },
{ label: "Proxmox VE", value: "pve" },
{ label: "Incus", value: "incus" },
diff --git a/frontend/pages/hosts/schema/query.ts b/frontend/pages/hosts/schema/query.ts
new file mode 100644
index 0000000..9a00c7a
--- /dev/null
+++ b/frontend/pages/hosts/schema/query.ts
@@ -0,0 +1,6 @@
+//
+
+export type MoveHostPayload = {
+ parentId: string | null;
+ hostId: string | string[];
+};
diff --git a/frontend/pages/server/page.tsx b/frontend/pages/server/page.tsx
index 11e4453..be6a582 100644
--- a/frontend/pages/server/page.tsx
+++ b/frontend/pages/server/page.tsx
@@ -80,6 +80,7 @@ export default function ServerPage() {
autoCapitalize="none"
keyboardType="url"
placeholder="https://"
+ onSubmitEditing={onSubmit}
/>
diff --git a/frontend/pages/terminal/new-session-page.tsx b/frontend/pages/terminal/new-session-page.tsx
new file mode 100644
index 0000000..a982587
--- /dev/null
+++ b/frontend/pages/terminal/new-session-page.tsx
@@ -0,0 +1,40 @@
+import { View, Text } from "react-native";
+import React from "react";
+import HostList from "../hosts/components/host-list";
+import { useQueryParams } from "@/hooks/useQueryParams";
+import { BackButton } from "@/components/ui/button";
+import { XStack } from "tamagui";
+
+type Params = {
+ parentId?: string | null;
+};
+
+const NewSessionPage = () => {
+ const queryParams = useQueryParams();
+ const parentId = queryParams.params?.parentId;
+
+ const setParentId = (id: string | null) => {
+ queryParams.push({ parentId: id || "" });
+ };
+
+ const onGoBack = () => {
+ if (!queryParams.goBack()) {
+ queryParams.replace({ parentId: "" });
+ }
+ };
+
+ return (
+ <>
+
+ {parentId ? : null}
+
+
+ >
+ );
+};
+
+export default NewSessionPage;
diff --git a/frontend/pages/terminal/page.tsx b/frontend/pages/terminal/page.tsx
index 0db15ed..6158071 100644
--- a/frontend/pages/terminal/page.tsx
+++ b/frontend/pages/terminal/page.tsx
@@ -4,11 +4,11 @@ import PagerView, { PagerViewRef } from "@/components/ui/pager-view";
import { useTermSession } from "@/stores/terminal-sessions";
import { Button, useMedia } from "tamagui";
import SessionTabs from "./components/session-tabs";
-import HostList from "../hosts/components/host-list";
import Drawer from "expo-router/drawer";
import { router } from "expo-router";
import Icons from "@/components/ui/icons";
import { useDebounceCallback } from "@/hooks/useDebounce";
+import NewSessionPage from "./new-session-page";
const TerminalPage = () => {
const pagerViewRef = useRef(null!);
@@ -58,7 +58,7 @@ const TerminalPage = () => {
/>
{sessions.length > 0 && media.gtSm ? : null}
- {!sessions.length ? : pagerView}
+ {!sessions.length ? : pagerView}
>
);
};
diff --git a/server/app/hosts/repository.go b/server/app/hosts/repository.go
index c65bb18..313fa52 100644
--- a/server/app/hosts/repository.go
+++ b/server/app/hosts/repository.go
@@ -23,19 +23,37 @@ func NewRepository(r *Hosts) *Hosts {
func (r *Hosts) GetAll(opt GetAllOpt) ([]*models.Host, error) {
query := r.db.Order("id DESC")
+ if len(opt.ID) > 0 {
+ query = query.Where("hosts.id IN (?)", opt.ID)
+ }
+
if opt.TeamID != "" {
query = query.Where("hosts.team_id = ?", opt.TeamID)
} else {
query = query.Where("hosts.owner_id = ? AND hosts.team_id IS NULL", r.User.ID)
}
+ if opt.ParentID != nil {
+ if *opt.ParentID != "" {
+ query = query.Where("hosts.parent_id = ?", *opt.ParentID)
+ } else {
+ query = query.Where("hosts.parent_id IS NULL")
+ }
+ }
+
var rows []*models.Host
ret := query.Find(&rows)
return rows, ret.Error
}
-func (r *Hosts) Get(id string) (*models.HostDecrypted, error) {
+func (r *Hosts) Get(id string) (*models.Host, error) {
+ var host models.Host
+ ret := r.db.Where("hosts.id = ?", id).First(&host)
+ return &host, ret.Error
+}
+
+func (r *Hosts) GetWithKeys(id string) (*models.HostDecrypted, error) {
var host models.Host
ret := r.db.Joins("Key").Joins("AltKey").Where("hosts.id = ?", id).First(&host)
if ret.Error != nil {
@@ -67,3 +85,7 @@ func (r *Hosts) Update(id string, item *models.Host) error {
func (r *Hosts) Delete(id string) error {
return r.db.Delete(&models.Host{Model: models.Model{ID: id}}).Error
}
+
+func (r *Hosts) SetParentId(id *string, hostIds []string) error {
+ return r.db.Model(&models.Host{}).Where("id IN (?)", hostIds).Update("parent_id", id).Error
+}
diff --git a/server/app/hosts/router.go b/server/app/hosts/router.go
index 7e1f563..9601d18 100644
--- a/server/app/hosts/router.go
+++ b/server/app/hosts/router.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
+ "strings"
"github.com/gofiber/fiber/v2"
"rul.sh/vaulterm/server/models"
@@ -17,10 +18,13 @@ func Router(app fiber.Router) {
router.Post("/", create)
router.Put("/:id", update)
router.Delete("/:id", delete)
+ router.Post("/move", move)
}
func getAll(c *fiber.Ctx) error {
teamId := c.Query("teamId")
+ parentId := c.Query("parentId")
+
user := utils.GetUser(c)
repo := NewRepository(&Hosts{User: user})
@@ -28,7 +32,7 @@ func getAll(c *fiber.Ctx) error {
return utils.ResponseError(c, errors.New("no access"), 403)
}
- rows, err := repo.GetAll(GetAllOpt{TeamID: teamId})
+ rows, err := repo.GetAll(GetAllOpt{TeamID: teamId, ParentID: &parentId})
if err != nil {
return utils.ResponseError(c, err, 500)
}
@@ -143,3 +147,55 @@ func delete(c *fiber.Ctx) error {
"message": "Successfully deleted",
})
}
+
+func move(c *fiber.Ctx) error {
+ user := utils.GetUser(c)
+ repo := NewRepository(&Hosts{User: user})
+
+ // validate request
+ var body MoveHostSchema
+ if err := c.BodyParser(&body); err != nil {
+ return utils.ResponseError(c, err, 500)
+ }
+ if body.HostID == "" {
+ return utils.ResponseError(c, errors.New("invalid request"), 400)
+ }
+
+ // get parent
+ var parentId *string
+
+ if body.ParentID != "" {
+ parent, err := repo.Get(body.ParentID)
+ if err != nil {
+ return utils.ResponseError(c, err, 500)
+ }
+ if !parent.CanWrite(&user.User) {
+ return utils.ResponseError(c, errors.New("no access"), 403)
+ }
+ parentId = &body.ParentID
+ }
+
+ // get hosts
+ hostIds := strings.Split(body.HostID, ",")
+ hosts, err := repo.GetAll(GetAllOpt{TeamID: body.TeamID, ID: hostIds})
+ if err != nil {
+ return utils.ResponseError(c, err, 500)
+ }
+
+ if len(hosts) != len(hostIds) {
+ return utils.ResponseError(c, errors.New("one or more hosts not found"), 400)
+ }
+
+ for _, host := range hosts {
+ if !host.CanWrite(&user.User) {
+ return utils.ResponseError(c, errors.New("no access"), 403)
+ }
+ }
+
+ // move the hosts to new parent
+ if err := repo.SetParentId(parentId, hostIds); err != nil {
+ return utils.ResponseError(c, err, 500)
+ }
+
+ return c.JSON(true)
+}
diff --git a/server/app/hosts/schema.go b/server/app/hosts/schema.go
index 1f96d66..b3d577c 100644
--- a/server/app/hosts/schema.go
+++ b/server/app/hosts/schema.go
@@ -16,5 +16,13 @@ type CreateHostSchema struct {
}
type GetAllOpt struct {
- TeamID string
+ TeamID string
+ ParentID *string
+ ID []string
+}
+
+type MoveHostSchema struct {
+ TeamID string `json:"teamId"`
+ ParentID string `json:"parentId"`
+ HostID string `json:"hostId"`
}
diff --git a/server/app/ws/stats/ssh.go b/server/app/ws/stats/ssh.go
index 4bdf653..23e6f3b 100644
--- a/server/app/ws/stats/ssh.go
+++ b/server/app/ws/stats/ssh.go
@@ -149,7 +149,13 @@ func getDiskUsage(client *lib.SSHClient, wg *sync.WaitGroup, result chan<- strin
return
}
- fields := strings.Fields(lines[1])
+ fields := strings.Fields(lines[len(lines)-2])
+ if len(fields) < 5 {
+ return
+ }
+ if !strings.HasPrefix(fields[0], "/") {
+ fields = append([]string{"/"}, fields...)
+ }
result <- fmt.Sprintf("\x03%s,%s,%s", fields[1], fields[2], fields[4])
}
diff --git a/server/app/ws/stats/stats.go b/server/app/ws/stats/stats.go
index d0899e6..e1b24ae 100644
--- a/server/app/ws/stats/stats.go
+++ b/server/app/ws/stats/stats.go
@@ -13,7 +13,7 @@ func HandleStats(c *websocket.Conn) {
user := utils.GetUserWs(c)
hostRepo := hosts.NewRepository(&hosts.Hosts{User: user})
- data, _ := hostRepo.Get(hostId)
+ data, _ := hostRepo.GetWithKeys(hostId)
if data == nil || !data.HasAccess(&user.User) {
c.WriteMessage(websocket.TextMessage, []byte("Host not found"))
diff --git a/server/app/ws/term/term.go b/server/app/ws/term/term.go
index 8f313a1..b1d9b71 100644
--- a/server/app/ws/term/term.go
+++ b/server/app/ws/term/term.go
@@ -17,7 +17,7 @@ func HandleTerm(c *websocket.Conn) {
user := utils.GetUserWs(c)
hostRepo := hosts.NewRepository(&hosts.Hosts{User: user})
- data, err := hostRepo.Get(hostId)
+ data, err := hostRepo.GetWithKeys(hostId)
if data == nil || !data.HasAccess(&user.User) {
log.Printf("Cannot find host! %v\n", err)
diff --git a/server/models/host.go b/server/models/host.go
index 309aa28..a5b5e9b 100644
--- a/server/models/host.go
+++ b/server/models/host.go
@@ -5,6 +5,7 @@ import (
)
const (
+ HostTypeGroup = "group"
HostTypeSSH = "ssh"
HostTypePVE = "pve"
HostTypePVENode = "pve_node"
diff --git a/server/utils/config.go b/server/utils/config.go
index 4a31f21..7066dc1 100644
--- a/server/utils/config.go
+++ b/server/utils/config.go
@@ -9,16 +9,18 @@ import (
)
func GetDataPath(resolveFile string) string {
+ dataDir := os.Getenv("DATA_DIR")
+ if dataDir != "" {
+ return filepath.Join(dataDir, resolveFile)
+ }
+
// Resolve the app directory
- execPath, err := os.Executable()
+ cwd, err := os.Getwd()
if err != nil {
return ""
}
- appDir := filepath.Dir(execPath)
- if resolveFile == "" {
- return appDir
- }
- return filepath.Join(appDir, resolveFile)
+
+ return filepath.Join(cwd, resolveFile)
}
func CheckAndCreateEnvFile() error {