mirror of
https://github.com/khairul169/vaulterm.git
synced 2026-09-15 17:03:30 +07:00
feat: add tags
This commit is contained in:
@@ -8,12 +8,16 @@ import { ScrollView, XStack } from "tamagui";
|
||||
import { FormSchema, formSchema, typeOptions } from "../schema/form";
|
||||
import { InputField } from "@/components/ui/input";
|
||||
import FormField from "@/components/ui/form";
|
||||
import { useSaveHost } from "../hooks/query";
|
||||
import { useSaveHost, useTags } from "../hooks/query";
|
||||
import { ErrorAlert } from "@/components/ui/alert";
|
||||
import Button from "@/components/ui/button";
|
||||
import { PVEFormFields } from "./pve";
|
||||
import { IncusFormFields } from "./incus";
|
||||
import { SSHFormFields } from "./ssh";
|
||||
import {
|
||||
SelectMultipleField,
|
||||
useSelectCreatableItems,
|
||||
} from "@/components/ui/select-multiple";
|
||||
|
||||
export const hostFormModal = createDisclosure<FormSchema>();
|
||||
|
||||
@@ -22,6 +26,8 @@ const HostForm = () => {
|
||||
const form = useZForm(formSchema, data);
|
||||
const isEditing = data?.id != null;
|
||||
const type = form.watch("type");
|
||||
const hostTags = useTags();
|
||||
const [tags, addTag] = useSelectCreatableItems(hostTags.data);
|
||||
|
||||
const saveMutation = useSaveHost();
|
||||
|
||||
@@ -53,6 +59,17 @@ const HostForm = () => {
|
||||
|
||||
{type !== "group" && (
|
||||
<>
|
||||
<FormField label="Tags">
|
||||
<SelectMultipleField
|
||||
form={form}
|
||||
name="tags"
|
||||
placeholder="Select Tags"
|
||||
items={tags}
|
||||
isCreatable
|
||||
onCreate={addTag}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Hostname">
|
||||
<InputField
|
||||
form={form}
|
||||
@@ -98,4 +115,4 @@ const HostForm = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default HostForm;
|
||||
export default React.memo(HostForm);
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from "react";
|
||||
import { MultiTapPressable } from "@/components/ui/pressable";
|
||||
import Icons from "@/components/ui/icons";
|
||||
import OSIcons from "@/components/ui/os-icons";
|
||||
import Badge from "@/components/ui/badge";
|
||||
|
||||
type HostItemProps = {
|
||||
host: any;
|
||||
@@ -26,12 +27,14 @@ const HostItem = ({
|
||||
numberOfTaps={2}
|
||||
onMultiTap={onMultiTap}
|
||||
onTap={onTap}
|
||||
h="100%"
|
||||
>
|
||||
<Card
|
||||
bordered
|
||||
p="$4"
|
||||
borderColor={selected ? "$blue8" : "$borderColor"}
|
||||
bg={selected ? "$blue3" : undefined}
|
||||
h="100%"
|
||||
>
|
||||
<XStack>
|
||||
{host.type === "group" ? (
|
||||
@@ -48,6 +51,15 @@ const HostItem = ({
|
||||
|
||||
<View flex={1}>
|
||||
<Text>{host.label}</Text>
|
||||
|
||||
{host.tags?.length > 0 && (
|
||||
<XStack mt="$1" gap="$1" flexWrap="wrap">
|
||||
{host.tags.map((i: any) => (
|
||||
<Badge key={i.name}>{i.name}</Badge>
|
||||
))}
|
||||
</XStack>
|
||||
)}
|
||||
|
||||
<Text fontSize="$3" mt="$2">
|
||||
{host.host}
|
||||
</Text>
|
||||
|
||||
@@ -14,6 +14,7 @@ type HostsListProps = {
|
||||
onParentIdChange?: (id: string | null) => void;
|
||||
selected?: string[];
|
||||
onSelectedChange?: (ids: string[]) => void;
|
||||
hideGroups?: boolean;
|
||||
};
|
||||
|
||||
const HostList = ({
|
||||
@@ -22,12 +23,15 @@ const HostList = ({
|
||||
onParentIdChange,
|
||||
selected = [],
|
||||
onSelectedChange,
|
||||
hideGroups = false,
|
||||
}: HostsListProps) => {
|
||||
const openSession = useTermSession((i) => i.push);
|
||||
const navigation = useNavigation();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const { data, isLoading } = useHosts({ parentId });
|
||||
const { data, isLoading } = useHosts({
|
||||
parentId: !search.length ? parentId : "none",
|
||||
});
|
||||
|
||||
const hostsList = useMemo(() => {
|
||||
let items = data || [];
|
||||
@@ -37,7 +41,8 @@ const HostList = ({
|
||||
const q = search.toLowerCase();
|
||||
return (
|
||||
item.label.toLowerCase().includes(q) ||
|
||||
item.host.toLowerCase().includes(q)
|
||||
item.host.toLowerCase().includes(q) ||
|
||||
item.tags.find((i: any) => i.name.toLowerCase().includes(q)) != null
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -65,7 +70,8 @@ const HostList = ({
|
||||
|
||||
const onEdit = (host: any) => {
|
||||
if (!allowEdit) return;
|
||||
hostFormModal.onOpen(host);
|
||||
const data = { ...host, tags: host.tags?.map((i: any) => i.name) };
|
||||
hostFormModal.onOpen(data);
|
||||
};
|
||||
|
||||
const onOpenTerminal = (host: any) => {
|
||||
@@ -103,7 +109,7 @@ const HostList = ({
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView>
|
||||
{groups.length > 0 && (
|
||||
{groups.length > 0 && !hideGroups && (
|
||||
<>
|
||||
<Text mx="$4">Groups</Text>
|
||||
<ItemList
|
||||
|
||||
@@ -66,3 +66,17 @@ export const useMoveHost = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useTags = () => {
|
||||
const teamId = useTeamId();
|
||||
return useQuery({
|
||||
queryKey: ["hosts/tags", teamId],
|
||||
queryFn: () => api("/hosts/tags", { params: { teamId } }),
|
||||
select: (data) => {
|
||||
return data?.rows?.map((row: any) => ({
|
||||
label: row.name,
|
||||
value: row.name,
|
||||
}));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ const groupSchema = baseFormSchema.merge(
|
||||
|
||||
const hostSchema = baseFormSchema.merge(
|
||||
z.object({
|
||||
tags: z.string().array(),
|
||||
host: hostnameShape(),
|
||||
port: z.coerce
|
||||
.number({ message: "Invalid port" })
|
||||
@@ -65,6 +66,7 @@ export type FormSchema = z.infer<typeof formSchema>;
|
||||
export const initialValues: FormSchema = {
|
||||
type: "ssh",
|
||||
host: "",
|
||||
tags: [],
|
||||
port: 22,
|
||||
label: "",
|
||||
keyId: "",
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
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<Params>();
|
||||
const parentId = queryParams.params?.parentId;
|
||||
|
||||
const setParentId = (id: string | null) => {
|
||||
queryParams.push({ parentId: id || "" });
|
||||
};
|
||||
|
||||
const onGoBack = () => {
|
||||
if (!queryParams.goBack()) {
|
||||
queryParams.replace({ parentId: "" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<XStack alignItems="center" h="$6">
|
||||
{parentId ? <BackButton onPress={onGoBack} /> : null}
|
||||
</XStack>
|
||||
<HostList
|
||||
allowEdit={false}
|
||||
parentId={parentId}
|
||||
onParentIdChange={setParentId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewSessionPage;
|
||||
@@ -8,7 +8,7 @@ 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";
|
||||
import HostList from "../hosts/components/host-list";
|
||||
|
||||
const TerminalPage = () => {
|
||||
const pagerViewRef = useRef<PagerViewRef>(null!);
|
||||
@@ -58,7 +58,11 @@ const TerminalPage = () => {
|
||||
/>
|
||||
|
||||
{sessions.length > 0 && media.gtSm ? <SessionTabs /> : null}
|
||||
{!sessions.length ? <NewSessionPage /> : pagerView}
|
||||
{!sessions.length ? (
|
||||
<HostList allowEdit={false} parentId="none" hideGroups />
|
||||
) : (
|
||||
pagerView
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user