Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions src/api/services/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ConfigSummary,
ConfigTypeRelationships
} from "../types/configs";
import { Property } from "../types/topology";

export * from "./configAccess";

Expand Down Expand Up @@ -164,6 +165,163 @@ export const getConfig = (id: string) =>
ConfigDB.get(`/config_detail?id=eq.${id}&select=*`)
);

export type ConfigItemPropertiesResponse = {
changed: boolean;
properties: Property[];
};

export type ConfigPropertyRow = {
id?: string;
config_id: string;
scraper_id?: string | null;
created_by?: string | null;
name: string;
label?: string;
tooltip?: string;
icon?: string;
property_type?: string;
color?: string;
display_order?: number;
headline?: boolean;
hidden?: boolean;
text?: string;
value?: number | string;
unit?: string;
max?: number;
min?: number;
status?: string;
link_url?: string;
link_label?: string;
link_icon?: string;
};

const getConfigProperties = async (configId: string) => {
const result = await getConfig(configId);
return result.data?.[0]?.properties ?? [];
};

const toConfigPropertyRow = (
configId: string,
createdBy: string,
property: Property
): ConfigPropertyRow => {
const firstLink = property.links?.[0];

return {
config_id: configId,
created_by: createdBy,
name: property.name,
label: property.label,
tooltip: property.tooltip,
icon: property.icon,
property_type: property.type,
color: property.color,
display_order: property.order,
headline: property.headline,
hidden: property.hidden,
text: property.text,
value:
property.value instanceof Date
? property.value.getTime()
: property.value,
unit: property.unit,
max: property.max,
min: property.min,
status: property.status,
link_url: property.link ?? property.url ?? firstLink?.url,
link_label: property.link_label ?? firstLink?.label
};
};

const toProperty = (row: ConfigPropertyRow): Property => ({
name: row.name,
label: row.label,
tooltip: row.tooltip,
icon: row.icon,
type: row.property_type as Property["type"],
color: row.color,
order: row.display_order,
headline: row.headline,
hidden: row.hidden,
text: row.text,
value: row.value,
unit: row.unit,
max: row.max,
min: row.min,
status: row.status,
link: row.link_url,
link_label: row.link_label,
links: row.link_url
? [
{
label: row.link_label ?? row.link_url,
url: row.link_url
}
]
: undefined
});

export const getManualConfigItemProperties = async (
configId: string,
createdBy: string
) => {
const result = await resolvePostGrestRequestWithPagination<
ConfigPropertyRow[]
>(
ConfigDB.get(
`/config_properties?config_id=eq.${configId}&created_by=eq.${createdBy}&order=display_order.asc,name.asc`
)
);

return result.data ?? [];
};

export const createConfigItemProperty = async (
configId: string,
createdBy: string,
property: Property
): Promise<ConfigItemPropertiesResponse> => {
await ConfigDB.post(
"/config_properties",
toConfigPropertyRow(configId, createdBy, property),
{ headers: { Prefer: "return=minimal" } }
);

return { changed: true, properties: await getConfigProperties(configId) };
};

export const updateConfigItemProperty = async (
propertyId: string,
configId: string,
createdBy: string,
property: Property
): Promise<ConfigItemPropertiesResponse> => {
const row: Partial<ConfigPropertyRow> = toConfigPropertyRow(
configId,
createdBy,
property
);
delete row.config_id;
delete row.created_by;

await ConfigDB.patch(`/config_properties?id=eq.${propertyId}`, row, {
headers: { Prefer: "return=minimal" }
});

return { changed: true, properties: await getConfigProperties(configId) };
};

export const deleteConfigItemProperty = async (
configId: string,
propertyId: string
) => {
await ConfigDB.delete(`/config_properties?id=eq.${propertyId}`);

return { changed: true, properties: await getConfigProperties(configId) };
};

export const configPropertyRowToProperty = toProperty;

export type ConfigsTagList = {
key: string;
value: any;
Expand Down
9 changes: 1 addition & 8 deletions src/api/types/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,7 @@ export interface ConfigItem extends Timestamped, Avatar, Agent, Costs {
playbook_runs?: number;
checks?: number;
};
properties?: {
icon: string;
name: string;
links: {
label: string;
url: string;
}[];
}[];
properties?: Property[] | null;
last_scraped_time?: string;
}

Expand Down
6 changes: 6 additions & 0 deletions src/api/types/topology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type Property = {
name: string;
icon?: string;
label?: string;
tooltip?: string;
type?: "url" | "badge" | "currency" | "text" | "age" | "hidden";
text?: string;
max?: number;
Expand All @@ -16,8 +17,13 @@ export type Property = {
value?: ValueType;
unit?: string;
color?: string;
order?: number;
hidden?: boolean;
status?: string;
namespace?: string;
url?: string;
link?: string;
link_label?: string;
links?: {
label: string;
url: string;
Expand Down
151 changes: 151 additions & 0 deletions src/components/Configs/Sidebar/AddConfigPropertyModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { createConfigItemProperty } from "@flanksource-ui/api/services/configs";
import { Property } from "@flanksource-ui/api/types/topology";
import FormikTextInput from "@flanksource-ui/components/Forms/Formik/FormikTextInput";
import {
toastError,
toastSuccess
} from "@flanksource-ui/components/Toast/toast";
import { useUser } from "@flanksource-ui/context";
import { Button } from "@flanksource-ui/ui/Buttons/Button";
import { Modal } from "@flanksource-ui/ui/Modal";
import { Field, Form, Formik } from "formik";

type Props = {
configId: string;
isOpen: boolean;
onClose: () => void;
onAdded?: (properties?: Property[]) => void;
};

type AddPropertyForm = {
name: string;
valueType: "text" | "value";
text: string;
value: string;
link: string;
link_label: string;
};

export default function AddConfigPropertyModal({
configId,
isOpen,
onClose,
onAdded
}: Props) {
const { user } = useUser();

return (
<Modal
title="Add Property"
size="very-small"
open={isOpen}
onClose={onClose}
>
<Formik<AddPropertyForm>
initialValues={{
name: "",
valueType: "text",
text: "",
value: "",
link: "",
link_label: ""
}}
onSubmit={async (values, formik) => {
if (!user?.id) {
toastError("Could not determine current user");
return;
}

if (!values.name) {
toastError("Please provide property name");
formik.setSubmitting(false);
return;
}

if (values.valueType === "text" && !values.text) {
toastError("Please provide property text");
formik.setSubmitting(false);
return;
}

if (values.valueType === "value" && values.value === "") {
toastError("Please provide property value");
formik.setSubmitting(false);
return;
}

try {
const newProperty: Property = {
name: values.name,
...(values.valueType === "value"
? { value: Number(values.value) }
: { text: values.text }),
...(values.link
? { link: values.link, link_label: values.link_label }
: {})
};

const result = await createConfigItemProperty(
configId,
user.id,
newProperty
);
toastSuccess("Property added");
onAdded?.(result?.properties);
onClose();
} catch (e) {
toastError((e as Error).message);
} finally {
formik.setSubmitting(false);
}
}}
>
{({ isSubmitting, values }) => (
<Form>
<div className="flex flex-col gap-4 p-2">
<FormikTextInput name="name" label="Name" required />
<label className="flex flex-col text-sm font-medium text-gray-700">
Value type
<Field
as="select"
name="valueType"
className="form-select mt-1 rounded border-gray-300 text-sm"
>
<option value="text">Text</option>
<option value="value">Number</option>
</Field>
</label>
{values.valueType === "value" ? (
<FormikTextInput
name="value"
label="Value"
type="number"
required
/>
) : (
<FormikTextInput name="text" label="Text" required />
)}
<FormikTextInput name="link" label="Link" />
<FormikTextInput name="link_label" label="Link label" />
</div>
<div className="flex items-center justify-end rounded-lg bg-gray-100 px-5 py-4">
<button
className="btn-secondary-base btn-secondary mr-4"
type="button"
onClick={onClose}
>
Cancel
</button>
<Button
type="submit"
text="Save"
className="btn-primary"
disabled={isSubmitting}
/>
</div>
</Form>
)}
</Formik>
</Modal>
);
}
Loading
Loading