Skip to content

Commit ed01c9c

Browse files
committed
feat(config): manage manual properties
Add config sidebar controls for users to add manual properties with either text or numeric values and delete only their own manually-added properties.\n\nWire the UI to the config item property update/delete RPCs and carry property ownership fields through config property types.
1 parent eca339f commit ed01c9c

7 files changed

Lines changed: 350 additions & 11 deletions

File tree

src/api/services/configs.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ConfigSummary,
1515
ConfigTypeRelationships
1616
} from "../types/configs";
17+
import { Property } from "../types/topology";
1718

1819
export * from "./configAccess";
1920

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

168+
export type UpdateConfigItemPropertiesResponse = {
169+
changed: boolean;
170+
properties: Property[];
171+
};
172+
173+
export const updateConfigItemProperties = async (
174+
configId: string,
175+
creatorType: "person" | "scraper",
176+
createdBy: string,
177+
properties: Property[]
178+
) => {
179+
const res = await ConfigDB.post<UpdateConfigItemPropertiesResponse[]>(
180+
"/rpc/update_config_item_properties",
181+
{
182+
p_config_id: configId,
183+
p_creator_type: creatorType,
184+
p_created_by: createdBy,
185+
p_properties: properties
186+
}
187+
);
188+
return res.data?.[0];
189+
};
190+
191+
export const deleteConfigItemProperty = async (
192+
configId: string,
193+
creatorType: "person" | "scraper",
194+
createdBy: string,
195+
propertyName: string
196+
) => {
197+
const res = await ConfigDB.post<UpdateConfigItemPropertiesResponse[]>(
198+
"/rpc/delete_config_item_property",
199+
{
200+
p_config_id: configId,
201+
p_creator_type: creatorType,
202+
p_created_by: createdBy,
203+
p_property_name: propertyName
204+
}
205+
);
206+
return res.data?.[0];
207+
};
208+
167209
export type ConfigsTagList = {
168210
key: string;
169211
value: any;

src/api/types/configs.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,7 @@ export interface ConfigItem extends Timestamped, Avatar, Agent, Costs {
8383
playbook_runs?: number;
8484
checks?: number;
8585
};
86-
properties?: {
87-
icon: string;
88-
name: string;
89-
links: {
90-
label: string;
91-
url: string;
92-
}[];
93-
}[];
86+
properties?: Property[] | null;
9487
last_scraped_time?: string;
9588
}
9689

src/api/types/topology.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export type Property = {
2222
label: string;
2323
url: string;
2424
}[];
25+
created_by?: string;
26+
creator_type?: "scraper" | "person" | string;
2527
};
2628

2729
export interface Component extends Timestamped, Namespaced {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { updateConfigItemProperties } from "@flanksource-ui/api/services/configs";
2+
import { Property } from "@flanksource-ui/api/types/topology";
3+
import FormikTextInput from "@flanksource-ui/components/Forms/Formik/FormikTextInput";
4+
import {
5+
toastError,
6+
toastSuccess
7+
} from "@flanksource-ui/components/Toast/toast";
8+
import { useUser } from "@flanksource-ui/context";
9+
import { Button } from "@flanksource-ui/ui/Buttons/Button";
10+
import { Modal } from "@flanksource-ui/ui/Modal";
11+
import { Field, Form, Formik } from "formik";
12+
13+
type Props = {
14+
configId: string;
15+
isOpen: boolean;
16+
onClose: () => void;
17+
onAdded?: (properties?: Property[]) => void;
18+
existingProperties?: Property[] | null;
19+
};
20+
21+
type AddPropertyForm = {
22+
name: string;
23+
valueType: "text" | "value";
24+
text: string;
25+
value: string;
26+
};
27+
28+
export default function AddConfigPropertyModal({
29+
configId,
30+
isOpen,
31+
onClose,
32+
onAdded,
33+
existingProperties
34+
}: Props) {
35+
const { user } = useUser();
36+
37+
const userProperties =
38+
existingProperties?.filter(
39+
(property) =>
40+
property.creator_type === "person" && property.created_by === user?.id
41+
) ?? [];
42+
43+
return (
44+
<Modal
45+
title="Add Property"
46+
size="very-small"
47+
open={isOpen}
48+
onClose={onClose}
49+
>
50+
<Formik<AddPropertyForm>
51+
initialValues={{ name: "", valueType: "text", text: "", value: "" }}
52+
onSubmit={async (values, formik) => {
53+
if (!user?.id) {
54+
toastError("Could not determine current user");
55+
return;
56+
}
57+
58+
if (!values.name) {
59+
toastError("Please provide property name");
60+
formik.setSubmitting(false);
61+
return;
62+
}
63+
64+
if (values.valueType === "text" && !values.text) {
65+
toastError("Please provide property text");
66+
formik.setSubmitting(false);
67+
return;
68+
}
69+
70+
if (values.valueType === "value" && values.value === "") {
71+
toastError("Please provide property value");
72+
formik.setSubmitting(false);
73+
return;
74+
}
75+
76+
try {
77+
const newProperty: Property =
78+
values.valueType === "value"
79+
? { name: values.name, value: Number(values.value) }
80+
: { name: values.name, text: values.text };
81+
82+
const incoming = [
83+
...userProperties.filter(
84+
(property) => property.name !== values.name
85+
),
86+
newProperty
87+
];
88+
89+
const result = await updateConfigItemProperties(
90+
configId,
91+
"person",
92+
user.id,
93+
incoming
94+
);
95+
toastSuccess("Property added");
96+
onAdded?.(result?.properties);
97+
onClose();
98+
} catch (e) {
99+
toastError((e as Error).message);
100+
} finally {
101+
formik.setSubmitting(false);
102+
}
103+
}}
104+
>
105+
{({ isSubmitting, values }) => (
106+
<Form>
107+
<div className="flex flex-col gap-4 p-2">
108+
<FormikTextInput name="name" label="Name" required />
109+
<label className="flex flex-col text-sm font-medium text-gray-700">
110+
Value type
111+
<Field
112+
as="select"
113+
name="valueType"
114+
className="form-select mt-1 rounded border-gray-300 text-sm"
115+
>
116+
<option value="text">Text</option>
117+
<option value="value">Number</option>
118+
</Field>
119+
</label>
120+
{values.valueType === "value" ? (
121+
<FormikTextInput
122+
name="value"
123+
label="Value"
124+
type="number"
125+
required
126+
/>
127+
) : (
128+
<FormikTextInput name="text" label="Text" required />
129+
)}
130+
</div>
131+
<div className="flex items-center justify-end rounded-lg bg-gray-100 px-5 py-4">
132+
<button
133+
className="btn-secondary-base btn-secondary mr-4"
134+
type="button"
135+
onClick={onClose}
136+
>
137+
Cancel
138+
</button>
139+
<Button
140+
type="submit"
141+
text="Save"
142+
className="btn-primary"
143+
disabled={isSubmitting}
144+
/>
145+
</div>
146+
</Form>
147+
)}
148+
</Formik>
149+
</Modal>
150+
);
151+
}

src/components/Configs/Sidebar/ConfigDetails.tsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ import TextSkeletonLoader from "@flanksource-ui/ui/SkeletonLoader/TextSkeletonLo
99
import { refreshButtonClickedTrigger } from "@flanksource-ui/ui/SlidingSideBar/SlidingSideBar";
1010
import dayjs from "dayjs";
1111
import { useAtom } from "jotai";
12-
import { useMemo, useEffect } from "react";
13-
import { FaExclamationTriangle, FaTrash } from "react-icons/fa";
12+
import { useEffect, useMemo, useState } from "react";
13+
import { FaEdit, FaExclamationTriangle, FaPlus, FaTrash } from "react-icons/fa";
1414
import { Link } from "react-router-dom";
15+
import { Tooltip } from "react-tooltip";
1516
import { InfoMessage } from "../../InfoMessage";
1617
import { Status } from "../../Status";
1718
import DisplayDetailsRow from "../../Utils/DisplayDetailsRow";
1819
import { DisplayGroupedProperties } from "../../Utils/DisplayGroupedProperties";
1920
import ConfigCostValue from "../ConfigCosts/ConfigCostValue";
2021
import ConfigsTypeIcon from "../ConfigsTypeIcon";
22+
import { IconButton } from "../../../ui/Buttons/IconButton";
23+
import AddConfigPropertyModal from "./AddConfigPropertyModal";
24+
import ManageConfigPropertiesModal from "./ManageConfigPropertiesModal";
2125
import { formatConfigLabels } from "./Utils/formatConfigLabels";
2226
import { MdOutlineSupportAgent } from "react-icons/md";
2327

@@ -26,6 +30,9 @@ type Props = {
2630
};
2731

2832
export function ConfigDetails({ configId }: Props) {
33+
const [isAddPropertyOpen, setIsAddPropertyOpen] = useState(false);
34+
const [isManagePropertiesOpen, setIsManagePropertiesOpen] = useState(false);
35+
2936
const {
3037
data: configDetails,
3138
isLoading,
@@ -252,8 +259,45 @@ export function ConfigDetails({ configId }: Props) {
252259

253260
<DisplayDetailsRow items={types} />
254261

262+
<div className="flex items-center justify-between py-1 text-sm text-gray-700">
263+
<span className="border-b border-dashed border-gray-500">
264+
Properties
265+
</span>
266+
<div className="flex items-center gap-2">
267+
<IconButton
268+
data-tooltip-id="edit-properties-tooltip"
269+
data-tooltip-content="Edit properties"
270+
icon={<FaEdit className="text-gray-600" size={14} />}
271+
onClick={() => setIsManagePropertiesOpen(true)}
272+
/>
273+
<IconButton
274+
data-tooltip-id="add-property-tooltip"
275+
data-tooltip-content="Add property"
276+
icon={<FaPlus className="text-gray-600" size={14} />}
277+
onClick={() => setIsAddPropertyOpen(true)}
278+
/>
279+
</div>
280+
<Tooltip id="edit-properties-tooltip" />
281+
<Tooltip id="add-property-tooltip" />
282+
</div>
255283
<DisplayGroupedProperties items={properties} />
256284

285+
<AddConfigPropertyModal
286+
configId={configId}
287+
isOpen={isAddPropertyOpen}
288+
onClose={() => setIsAddPropertyOpen(false)}
289+
existingProperties={configDetails.properties}
290+
onAdded={() => refetchConfig()}
291+
/>
292+
293+
<ManageConfigPropertiesModal
294+
configId={configId}
295+
isOpen={isManagePropertiesOpen}
296+
onClose={() => setIsManagePropertiesOpen(false)}
297+
existingProperties={configDetails.properties}
298+
onChanged={() => refetchConfig()}
299+
/>
300+
257301
{!isCostsEmpty(configDetails) && (
258302
<DisplayDetailsRow
259303
items={[

0 commit comments

Comments
 (0)