mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 08:39:37 +07:00
feat: add tags
This commit is contained in:
parent
95f9f76edd
commit
b0932c39b1
18
frontend/components/ui/badge.tsx
Normal file
18
frontend/components/ui/badge.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { GetProps, Text, View } from "tamagui";
|
||||
|
||||
type BadgeProps = GetProps<typeof View>;
|
||||
|
||||
const Badge = ({ children, ...props }: BadgeProps) => {
|
||||
return (
|
||||
<View px={5} py={1} flexShrink={0} bg="$blue6" borderRadius="$3" {...props}>
|
||||
{typeof children === "string" ? (
|
||||
<Text fontSize="$2">{children}</Text>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badge;
|
@ -56,7 +56,7 @@ export const GridLayout = <T extends GridItem>({
|
||||
}, [columns]);
|
||||
|
||||
return (
|
||||
<View flexDirection="row" flexWrap="wrap" {...props}>
|
||||
<View flexDirection="row" flexWrap="wrap" alignItems="stretch" {...props}>
|
||||
{data?.map((item, idx) => (
|
||||
<View key={item.key} p={gap} flexShrink={0} {...basisProps}>
|
||||
{renderItem(item, idx)}
|
||||
|
@ -27,17 +27,10 @@ const Modal = ({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} modal>
|
||||
<Adapt when="sm" platform="touch">
|
||||
<Sheet
|
||||
animation="quick"
|
||||
zIndex={999}
|
||||
modal
|
||||
dismissOnSnapToBottom
|
||||
snapPoints={[40, 60, 0]}
|
||||
// disableDrag
|
||||
>
|
||||
<Sheet animation="quick" zIndex={999} modal dismissOnSnapToBottom>
|
||||
<Sheet.Overlay
|
||||
opacity={0.1}
|
||||
animation="quick"
|
||||
animation="quickest"
|
||||
enterStyle={{ opacity: 0 }}
|
||||
exitStyle={{ opacity: 0 }}
|
||||
zIndex={0}
|
||||
@ -52,7 +45,7 @@ const Modal = ({
|
||||
<Dialog.Overlay
|
||||
key="overlay"
|
||||
animation="quickest"
|
||||
opacity={0.5}
|
||||
opacity={0.2}
|
||||
enterStyle={{ opacity: 0 }}
|
||||
exitStyle={{ opacity: 0 }}
|
||||
/>
|
||||
|
257
frontend/components/ui/select-multiple.tsx
Normal file
257
frontend/components/ui/select-multiple.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
import React, { forwardRef, useMemo, useState } from "react";
|
||||
import { Controller, FieldValues } from "react-hook-form";
|
||||
import {
|
||||
Adapt,
|
||||
Button,
|
||||
Dialog,
|
||||
Input,
|
||||
ListItem,
|
||||
ScrollView,
|
||||
Sheet,
|
||||
Text,
|
||||
View,
|
||||
} from "tamagui";
|
||||
import { FormFieldBaseProps } from "./utility";
|
||||
import { ErrorMessage } from "./form";
|
||||
import Icons from "./icons";
|
||||
|
||||
export type SelectItem = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type SelectMultipleProps = React.ComponentPropsWithoutRef<typeof ListItem> & {
|
||||
items?: SelectItem[] | null;
|
||||
value?: string[];
|
||||
defaultValue?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
placeholder?: string;
|
||||
isCreatable?: boolean;
|
||||
onCreate?: (value: string) => void;
|
||||
};
|
||||
|
||||
type SelectMultipleRef = React.ElementRef<typeof ListItem>;
|
||||
|
||||
const SelectMultiple = forwardRef<SelectMultipleRef, SelectMultipleProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
items,
|
||||
value,
|
||||
defaultValue,
|
||||
onChange,
|
||||
placeholder = "Select...",
|
||||
isCreatable = false,
|
||||
onCreate,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const itemList = useMemo(() => {
|
||||
if (!items?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let list = [...items];
|
||||
|
||||
const searchText = search.toLowerCase().trim();
|
||||
if (searchText?.length > 0) {
|
||||
list = list.filter((i) => i.label.toLowerCase().includes(searchText));
|
||||
}
|
||||
|
||||
return list.sort((a) => (value?.includes(a.value) ? -1 : 1));
|
||||
}, [items, search, value]);
|
||||
|
||||
const onCreateNew = () => {
|
||||
const newItem = search.trim();
|
||||
onCreate?.(newItem);
|
||||
onItemPress(newItem);
|
||||
setSearch("");
|
||||
};
|
||||
|
||||
const onItemPress = (key: string) => {
|
||||
const curValues = [...(value || [])];
|
||||
const idx = curValues.indexOf(key);
|
||||
if (idx >= 0) {
|
||||
curValues.splice(idx, 1);
|
||||
onChange?.(curValues);
|
||||
} else {
|
||||
curValues.push(key);
|
||||
onChange?.(curValues);
|
||||
}
|
||||
};
|
||||
|
||||
const onEnter = () => {
|
||||
if (itemList.length > 0) {
|
||||
onItemPress(itemList[0].value);
|
||||
setSearch("");
|
||||
return;
|
||||
}
|
||||
|
||||
const newItem = search.trim();
|
||||
if (isCreatable && newItem.length > 0) {
|
||||
return onCreateNew();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Trigger asChild {...restProps}>
|
||||
<ListItem
|
||||
ref={ref}
|
||||
backgrounded
|
||||
hoverTheme
|
||||
pressTheme
|
||||
borderRadius="$4"
|
||||
tabIndex={0}
|
||||
focusVisibleStyle={{
|
||||
outlineStyle: "solid",
|
||||
outlineWidth: 2,
|
||||
outlineColor: "$outlineColor",
|
||||
}}
|
||||
borderWidth={1}
|
||||
title={value?.join(", ") || placeholder}
|
||||
/>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Adapt when="sm" platform="touch">
|
||||
<Sheet animation="quick" zIndex={999} modal dismissOnSnapToBottom>
|
||||
<Sheet.Overlay
|
||||
opacity={0.1}
|
||||
animation="quickest"
|
||||
enterStyle={{ opacity: 0 }}
|
||||
exitStyle={{ opacity: 0 }}
|
||||
zIndex={0}
|
||||
/>
|
||||
<Sheet.Frame>
|
||||
<Adapt.Contents />
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</Adapt>
|
||||
|
||||
<Dialog.Portal zIndex={999}>
|
||||
<Dialog.Overlay
|
||||
key="overlay"
|
||||
animation="quickest"
|
||||
opacity={0.2}
|
||||
enterStyle={{ opacity: 0 }}
|
||||
exitStyle={{ opacity: 0 }}
|
||||
/>
|
||||
|
||||
<Dialog.Content
|
||||
bordered
|
||||
key="content"
|
||||
elevate
|
||||
animateOnly={["transform", "opacity"]}
|
||||
animation={["quickest", { opacity: { overshootClamping: true } }]}
|
||||
enterStyle={{ x: 0, opacity: 0, scale: 0.95 }}
|
||||
exitStyle={{ x: 0, opacity: 0, scale: 0.98 }}
|
||||
p="$1"
|
||||
w="90%"
|
||||
maxWidth={400}
|
||||
height="80%"
|
||||
maxHeight={400}
|
||||
>
|
||||
<View p="$3">
|
||||
<Dialog.Title fontWeight="normal" fontSize="$7">
|
||||
{placeholder}
|
||||
</Dialog.Title>
|
||||
<Input
|
||||
mt="$1"
|
||||
placeholder="Search..."
|
||||
autoFocus
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
onSubmitEditing={onEnter}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Dialog.Close asChild>
|
||||
<Button
|
||||
position="absolute"
|
||||
top="$2"
|
||||
right="$2"
|
||||
size="$3"
|
||||
bg="$colorTransparent"
|
||||
circular
|
||||
icon={<Icons name="close" size={16} />}
|
||||
/>
|
||||
</Dialog.Close>
|
||||
|
||||
<ScrollView>
|
||||
{!itemList?.length && !isCreatable ? (
|
||||
<Text textAlign="center" my="$2">
|
||||
No results
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{!itemList?.length && isCreatable && search.trim().length > 0 ? (
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
onPress={onCreateNew}
|
||||
title={`Create "${search}"`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{itemList?.map((item) => (
|
||||
<ListItem
|
||||
key={item.value}
|
||||
bg="$colorTransparent"
|
||||
hoverTheme
|
||||
pressTheme
|
||||
onPress={() => onItemPress(item.value)}
|
||||
iconAfter={
|
||||
value?.includes(item.value) ? (
|
||||
<Icons name="check" size={16} />
|
||||
) : undefined
|
||||
}
|
||||
title={item.label}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type SelectMultipleFieldProps<T extends FieldValues> = FormFieldBaseProps<T> &
|
||||
SelectMultipleProps;
|
||||
|
||||
export const SelectMultipleField = <T extends FieldValues>({
|
||||
form,
|
||||
name,
|
||||
...props
|
||||
}: SelectMultipleFieldProps<T>) => (
|
||||
<Controller
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field, fieldState }) => (
|
||||
<>
|
||||
<SelectMultiple w="auto" f={1} {...field} {...props} />
|
||||
<ErrorMessage error={fieldState.error} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
export const useSelectCreatableItems = (initialItems?: SelectItem[] | null) => {
|
||||
const [items, setItems] = useState<SelectItem[]>([]);
|
||||
const itemsCombined = useMemo(() => {
|
||||
return [...items, ...(initialItems || [])];
|
||||
}, [items, initialItems]);
|
||||
|
||||
const addItem = (value: string) => {
|
||||
const idx = itemsCombined.findIndex((i) => i.value === value);
|
||||
if (idx >= 0) {
|
||||
return;
|
||||
}
|
||||
setItems([...items, { label: value, value }]);
|
||||
};
|
||||
|
||||
return [itemsCombined, addItem, setItems] as const;
|
||||
};
|
||||
|
||||
export default SelectMultiple;
|
@ -46,7 +46,7 @@ const Select = forwardRef<SelectRef, SelectProps>(
|
||||
<Sheet native modal dismissOnSnapToBottom snapPoints={[40, 60, 80]}>
|
||||
<Sheet.Overlay
|
||||
opacity={0.1}
|
||||
animation="quick"
|
||||
animation="quickest"
|
||||
enterStyle={{ opacity: 0 }}
|
||||
exitStyle={{ opacity: 0 }}
|
||||
zIndex={0}
|
||||
|
@ -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
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -21,7 +21,9 @@ func NewRepository(r *Hosts) *Hosts {
|
||||
}
|
||||
|
||||
func (r *Hosts) GetAll(opt GetAllOpt) ([]*models.Host, error) {
|
||||
query := r.db.Order("id DESC")
|
||||
query := r.db.
|
||||
Preload("Tags").
|
||||
Order("id DESC")
|
||||
|
||||
if len(opt.ID) > 0 {
|
||||
query = query.Where("hosts.id IN (?)", opt.ID)
|
||||
@ -33,7 +35,7 @@ func (r *Hosts) GetAll(opt GetAllOpt) ([]*models.Host, error) {
|
||||
query = query.Where("hosts.owner_id = ? AND hosts.team_id IS NULL", r.User.ID)
|
||||
}
|
||||
|
||||
if opt.ParentID != nil {
|
||||
if opt.ParentID != nil && *opt.ParentID != "none" {
|
||||
if *opt.ParentID != "" {
|
||||
query = query.Where("hosts.parent_id = ?", *opt.ParentID)
|
||||
} else {
|
||||
@ -89,3 +91,18 @@ func (r *Hosts) Delete(id string) 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
|
||||
}
|
||||
|
||||
func (r *Hosts) GetAvailableTags(teamId string) (*[]models.HostTag, error) {
|
||||
query := r.db.Model(&models.HostTag{}).
|
||||
Joins("JOIN hosts ON hosts.id = host_tags.host_id").
|
||||
Distinct("host_tags.name")
|
||||
|
||||
if teamId != "" {
|
||||
query = query.Where("hosts.team_id = ?", teamId)
|
||||
} else {
|
||||
query = query.Where("hosts.owner_id = ? AND hosts.team_id IS NULL", r.User.ID)
|
||||
}
|
||||
|
||||
var result []models.HostTag
|
||||
return &result, query.Find(&result).Error
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ func Router(app fiber.Router) {
|
||||
router.Put("/:id", update)
|
||||
router.Delete("/:id", delete)
|
||||
router.Post("/move", move)
|
||||
router.Get("/tags", getTags)
|
||||
}
|
||||
|
||||
func getAll(c *fiber.Ctx) error {
|
||||
@ -69,6 +70,11 @@ func create(c *fiber.Ctx) error {
|
||||
AltKeyID: body.AltKeyID,
|
||||
}
|
||||
|
||||
item.Tags = []*models.HostTag{}
|
||||
for _, tag := range body.Tags {
|
||||
item.Tags = append(item.Tags, &models.HostTag{Name: tag})
|
||||
}
|
||||
|
||||
osName, err := tryConnect(c, item)
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, fmt.Errorf("cannot connect to the host: %s", err), 500)
|
||||
@ -113,16 +119,28 @@ func update(c *fiber.Ctx) error {
|
||||
AltKeyID: body.AltKeyID,
|
||||
}
|
||||
|
||||
osName, err := tryConnect(c, item)
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, fmt.Errorf("cannot connect to the host: %s", err), 500)
|
||||
}
|
||||
item.OS = osName
|
||||
// osName, err := tryConnect(c, item)
|
||||
// if err != nil {
|
||||
// return utils.ResponseError(c, fmt.Errorf("cannot connect to the host: %s", err), 500)
|
||||
// }
|
||||
// item.OS = osName
|
||||
|
||||
if err := repo.Update(id, item); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
tags := []models.HostTag{}
|
||||
for _, tag := range body.Tags {
|
||||
tags = append(tags, models.HostTag{
|
||||
Name: tag,
|
||||
})
|
||||
}
|
||||
|
||||
if err := repo.db.Unscoped().Model(&item).
|
||||
Association("Tags").Unscoped().Replace(&tags); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(item)
|
||||
}
|
||||
|
||||
@ -200,3 +218,18 @@ func move(c *fiber.Ctx) error {
|
||||
|
||||
return c.JSON(true)
|
||||
}
|
||||
|
||||
func getTags(c *fiber.Ctx) error {
|
||||
teamId := c.Query("teamId")
|
||||
user := lib.GetUser(c)
|
||||
repo := NewRepository(&Hosts{User: user})
|
||||
|
||||
rows, err := repo.GetAvailableTags(teamId)
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"rows": rows,
|
||||
})
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ type CreateHostSchema struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Metadata datatypes.JSONMap `json:"metadata"`
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
TeamID *string `json:"teamId"`
|
||||
ParentID *string `json:"parentId"`
|
||||
|
@ -10,6 +10,7 @@ var Models = []interface{}{
|
||||
&models.UserAccount{},
|
||||
&models.Keychain{},
|
||||
&models.Host{},
|
||||
&models.HostTag{},
|
||||
&models.Team{},
|
||||
&models.TeamMembers{},
|
||||
&models.TermSession{},
|
||||
|
@ -28,6 +28,7 @@ type Host struct {
|
||||
Port int `json:"port" gorm:"type:smallint"`
|
||||
OS string `json:"os" gorm:"type:varchar(32)"`
|
||||
Metadata datatypes.JSONMap `json:"metadata"`
|
||||
Tags []*HostTag `json:"tags" gorm:"foreignKey:HostID"`
|
||||
|
||||
ParentID *string `json:"parentId" gorm:"type:varchar(26)"`
|
||||
Parent *Host `json:"parent" gorm:"foreignKey:ParentID"`
|
||||
@ -40,6 +41,12 @@ type Host struct {
|
||||
SoftDeletes
|
||||
}
|
||||
|
||||
type HostTag struct {
|
||||
HostID string `gorm:"primarykey;type:varchar(26)" json:"-"`
|
||||
Host *Host `gorm:"foreignKey:HostID;references:ID" json:"-"`
|
||||
Name string `gorm:"primarykey;type:varchar(64)" json:"name"`
|
||||
}
|
||||
|
||||
type HostDecrypted struct {
|
||||
Host
|
||||
Key map[string]interface{}
|
||||
|
Loading…
x
Reference in New Issue
Block a user