From fe74d4b4651bff0172bf2f0eb1781b7d8f1118c1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:28:07 -0700 Subject: [PATCH 01/23] add tag type --- client/src/Types/Tag.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 client/src/Types/Tag.ts diff --git a/client/src/Types/Tag.ts b/client/src/Types/Tag.ts new file mode 100644 index 000000000..bab6f740a --- /dev/null +++ b/client/src/Types/Tag.ts @@ -0,0 +1,8 @@ +export interface Tag { + id: string; + teamId: string; + name: string; + color: string; + createdAt: string; + updatedAt: string; +} From 1f7344f2e6606cf198225e7c22e8dbb4a98ac112 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:28:26 -0700 Subject: [PATCH 02/23] add Tags pages --- .../src/Pages/Tags/components/TagsTable.tsx | 103 ++++++++++++++++ client/src/Pages/Tags/create/index.tsx | 116 ++++++++++++++++++ client/src/Pages/Tags/index.tsx | 72 +++++++++++ 3 files changed, 291 insertions(+) create mode 100644 client/src/Pages/Tags/components/TagsTable.tsx create mode 100644 client/src/Pages/Tags/create/index.tsx create mode 100644 client/src/Pages/Tags/index.tsx diff --git a/client/src/Pages/Tags/components/TagsTable.tsx b/client/src/Pages/Tags/components/TagsTable.tsx new file mode 100644 index 000000000..c2cbd24d3 --- /dev/null +++ b/client/src/Pages/Tags/components/TagsTable.tsx @@ -0,0 +1,103 @@ +import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu"; +import Typography from "@mui/material/Typography"; +import type { Header } from "@/Components/design-elements/Table"; +import { Table, Chip } from "@/Components/design-elements"; +import { Pagination } from "@/Components/design-elements/Table"; +import { useClientPagination } from "@/Hooks/useClientPagination"; + +import type { Tag } from "@/Types/Tag"; +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material"; + +interface TagsTableProps { + tags: Tag[]; + setSelectedTag: Function; +} + +export const TagsTable = ({ tags, setSelectedTag }: TagsTableProps) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + const theme = useTheme(); + const { pagedRows, paginationProps } = useClientPagination(tags); + + const getActions = (tag: Tag): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("pages.common.monitors.actions.configure"), + action: () => { + navigate(`/tags/configure/${tag.id}`); + }, + closeMenu: true, + }, + + { + id: 2, + label: ( + + {t("pages.common.monitors.actions.delete")} + + ), + action: async () => { + setSelectedTag(tag); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: t("common.table.headers.name"), + render: (row) => { + return {row?.name}; + }, + }, + + { + id: "color", + content: t("pages.tags.table.headers.color"), + render: (row) => { + return {row?.color}; + }, + }, + { + id: "chip", + content: t("pages.tags.table.headers.chip"), + render: (row) => { + return ( + + ); + }, + }, + + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + return ( + <> + { + navigate(`/tags/configure/${row.id}`); + }} + /> + {tags.length > 0 && } + + ); +}; diff --git a/client/src/Pages/Tags/create/index.tsx b/client/src/Pages/Tags/create/index.tsx new file mode 100644 index 000000000..9ce7ca7c4 --- /dev/null +++ b/client/src/Pages/Tags/create/index.tsx @@ -0,0 +1,116 @@ +import { BasePage, ConfigBox } from "@/Components/design-elements"; +import { TextField, Button, ColorInput } from "@/Components/inputs"; +import Stack from "@mui/material/Stack"; +import { useTheme } from "@mui/material/styles"; + +import { useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useGet, usePost, usePatch } from "@/Hooks/UseApi"; +import type { TagFormData } from "@/Validation/tag"; +import type { Tag } from "@/Types/Tag"; +import { useTranslation } from "react-i18next"; +import { useTagsForm } from "@/Hooks/useTagsForm"; + +const TagsCreatePage = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + const { tagId } = useParams<{ tagId: string }>(); + const isEditMode = Boolean(tagId); + + const { data: existingTag } = useGet(isEditMode ? `/tags/${tagId}` : null); + + const { post, loading: isSubmitting } = usePost(); + const { patch, loading: isPatching } = usePatch(); + + const { schema, defaults } = useTagsForm({ data: existingTag }); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); + + const { control, reset, handleSubmit } = form; + + useEffect(() => { + reset(defaults); + }, [defaults, reset]); + + const onSubmit = async (data: TagFormData) => { + const result = isEditMode + ? await patch(`/tags/${tagId}`, data) + : await post("/tags", data); + if (result) { + navigate("/tags"); + } + }; + + return ( + + ( + + )} + /> + } + /> + ( + + )} + /> + } + /> + + + + + + ); +}; + +export default TagsCreatePage; diff --git a/client/src/Pages/Tags/index.tsx b/client/src/Pages/Tags/index.tsx new file mode 100644 index 000000000..9509cfc4b --- /dev/null +++ b/client/src/Pages/Tags/index.tsx @@ -0,0 +1,72 @@ +import { BasePageWithStates } from "@/Components/design-elements"; +import { TagsTable } from "@/Pages/Tags/components/TagsTable"; +import { Dialog } from "@/Components/inputs"; +import { HeaderCreate } from "@/Components/common"; + +import { useState } from "react"; +import { useGet, useDelete } from "@/Hooks/UseApi"; +import { useTranslation } from "react-i18next"; +import type { Tag } from "@/Types/Tag"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; + +const TagsPage = () => { + const { t } = useTranslation(); + const isAdmin = useIsAdmin(); + + const [selectedTag, setSelectedTag] = useState(null); + const isDialogOpen = Boolean(selectedTag); + + const { + data: tags, + isLoading, + isValidating, + error, + refetch, + } = useGet("/tags/team", {}, { keepPreviousData: true }); + + const { deleteFn, loading: isDeleting } = useDelete(); + + const handleConfirm = async () => { + if (!selectedTag) return; + await deleteFn(`/tags/${selectedTag.id}`); + setSelectedTag(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedTag(null); + }; + + return ( + + + + + + ); +}; + +export default TagsPage; From 8aa55885e530a71a4c27350c5ba55129cc7ea71e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:28:39 -0700 Subject: [PATCH 03/23] add tags validaiton --- client/src/Validation/tag.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 client/src/Validation/tag.ts diff --git a/client/src/Validation/tag.ts b/client/src/Validation/tag.ts new file mode 100644 index 000000000..96d4c6c94 --- /dev/null +++ b/client/src/Validation/tag.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const tagSchema = z.object({ + name: z.string().min(1, "Please enter a tag name"), + color: z + .string() + .regex(/^#([0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value (e.g. #13715B)"), +}); + +export type TagFormData = z.infer; From e96de364c0b4d2751d844bc69d8eac1a75157c33 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:28:55 -0700 Subject: [PATCH 04/23] add form hook for tags --- client/src/Hooks/useTagsForm.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 client/src/Hooks/useTagsForm.ts diff --git a/client/src/Hooks/useTagsForm.ts b/client/src/Hooks/useTagsForm.ts new file mode 100644 index 000000000..8e64f2ef0 --- /dev/null +++ b/client/src/Hooks/useTagsForm.ts @@ -0,0 +1,18 @@ +import { useMemo } from "react"; +import { tagSchema, type TagFormData } from "@/Validation/tag"; +import type { Tag } from "@/Types/Tag"; + +interface UseTagsFormOptions { + data?: Tag | null; +} + +export const useTagsForm = ({ data }: UseTagsFormOptions) => { + return useMemo(() => { + const defaults: TagFormData = { + name: data?.name ?? "", + color: data?.color ?? "#13715B", + }; + + return { schema: tagSchema, defaults }; + }, [data]); +}; From e6a0f89c684c87f1e8883641e17a352ee41577d2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:29:06 -0700 Subject: [PATCH 05/23] add strings --- client/src/locales/en.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index b964eec84..0428a76d4 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -876,6 +876,37 @@ "description": "Track server performance, identify bottlenecks, and ensure reliability with real-time infrastructure monitoring." } }, + "tags": { + "header": { + "title": "Tags", + "description": "Organize your monitors with tags" + }, + "fallback": { + "title": "No tags yet", + "description": "Organize your monitors with tags. Once you create a tag and assign it to a monitor, you'll be able to filter by that tag across the app and see tag details here.", + "actionButton": "Create a tag" + }, + "table": { + "headers": { + "color": "Color", + "chip": "Chip" + } + }, + "form": { + "name": { + "title": "Tag name", + "description": "Choose a name for your tag", + "optionName": "Tag name", + "placeholder": "e.g. Group 1" + }, + "color": { + "title": "Tag color", + "description": "Choose a color for your tag", + "optionName": "Tag color", + "placeholder": "e.g. #13715B" + } + } + }, "logs": { "fallback": { "title": "No logs yet", From 5a187cd1c9ac3ce425e2434c26ab60f0688c1d74 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:29:13 -0700 Subject: [PATCH 06/23] add new chip component --- .../src/Components/design-elements/Chip.tsx | 30 +++++++++++++++++++ .../src/Components/design-elements/index.tsx | 1 + client/src/Components/sidebar/Menu.tsx | 5 ++++ 3 files changed, 36 insertions(+) create mode 100644 client/src/Components/design-elements/Chip.tsx diff --git a/client/src/Components/design-elements/Chip.tsx b/client/src/Components/design-elements/Chip.tsx new file mode 100644 index 000000000..c5a902feb --- /dev/null +++ b/client/src/Components/design-elements/Chip.tsx @@ -0,0 +1,30 @@ +import MuiChip from "@mui/material/Chip"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material"; +interface ChipProps { + text: string; + color?: string; +} + +export const Chip = ({ text, color }: ChipProps) => { + const theme = useTheme(); + const textColor = color + ? theme.palette.getContrastText(color) + : theme.palette.text.primary; + return ( + + {text} + + } + sx={{ + backgroundColor: color, + }} + /> + ); +}; diff --git a/client/src/Components/design-elements/index.tsx b/client/src/Components/design-elements/index.tsx index 130ccd831..0954041cd 100644 --- a/client/src/Components/design-elements/index.tsx +++ b/client/src/Components/design-elements/index.tsx @@ -27,3 +27,4 @@ export * from "./OfflineBanner"; export * from "./Avatar"; export * from "./StatusCodeLabel"; export * from "./StrategyBadge"; +export * from "./Chip"; diff --git a/client/src/Components/sidebar/Menu.tsx b/client/src/Components/sidebar/Menu.tsx index d3e9d46ee..dae273575 100644 --- a/client/src/Components/sidebar/Menu.tsx +++ b/client/src/Components/sidebar/Menu.tsx @@ -41,6 +41,11 @@ export const getMenu = (t: Function) => { path: "notifications", icon: , }, + { + name: t("components.sidebar.menu.notifications"), + path: "tags", + icon: , + }, { name: t("components.sidebar.menu.checks"), path: "checks", From 02640c31558d512e3c7c12d26b46c2cdeeabef62 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:29:25 -0700 Subject: [PATCH 07/23] add routes --- client/src/Routes/index.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/client/src/Routes/index.tsx b/client/src/Routes/index.tsx index 20869c47c..c3091a924 100644 --- a/client/src/Routes/index.tsx +++ b/client/src/Routes/index.tsx @@ -31,9 +31,14 @@ import CreateStatus from "@/Pages/StatusPage/Create/"; import StatusPages from "@/Pages/StatusPage/StatusPages"; import Status from "@/Pages/StatusPage/Status"; +// Notifications import Notifications from "@/Pages/Notifications"; import CreateNotifications from "@/Pages/Notifications/create"; +// Tags +import Tags from "@/Pages/Tags"; +import CreateTags from "@/Pages/Tags/create"; + // Settings import Account from "@/Pages/Account"; import EditUser from "@/Pages/Account/EditUser"; @@ -159,6 +164,19 @@ const Routes = () => { path="notifications/configure/:notificationId" element={} /> + } + /> + } + /> + + } + /> Date: Wed, 20 May 2026 10:45:36 -0700 Subject: [PATCH 08/23] add a colored label --- .../design-elements/StatusLabel.tsx | 20 +++++++++++++++++++ client/src/Components/sidebar/Menu.tsx | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/client/src/Components/design-elements/StatusLabel.tsx b/client/src/Components/design-elements/StatusLabel.tsx index d50058378..ee9a749c2 100644 --- a/client/src/Components/design-elements/StatusLabel.tsx +++ b/client/src/Components/design-elements/StatusLabel.tsx @@ -93,3 +93,23 @@ export const ValueLabel = ({ value, text }: { value: ValueType; text: string }) ); }; + +export const ColoredLabel = ({ text, color }: { text: string; color: string }) => { + const theme = useTheme(); + + return ( + + {text} + + ); +}; diff --git a/client/src/Components/sidebar/Menu.tsx b/client/src/Components/sidebar/Menu.tsx index dae273575..8064a18ac 100644 --- a/client/src/Components/sidebar/Menu.tsx +++ b/client/src/Components/sidebar/Menu.tsx @@ -17,6 +17,7 @@ import { User, Lock, Users, + Tag, } from "lucide-react"; export const getMenu = (t: Function) => { @@ -42,9 +43,9 @@ export const getMenu = (t: Function) => { icon: , }, { - name: t("components.sidebar.menu.notifications"), + name: t("components.sidebar.menu.tags"), path: "tags", - icon: , + icon: , }, { name: t("components.sidebar.menu.checks"), From dc80b3cafce38010b5f2170fb62670b0c2b5d98d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:45:45 -0700 Subject: [PATCH 09/23] add tag to table --- client/src/Pages/Tags/components/TagsTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/Tags/components/TagsTable.tsx b/client/src/Pages/Tags/components/TagsTable.tsx index c2cbd24d3..a84922140 100644 --- a/client/src/Pages/Tags/components/TagsTable.tsx +++ b/client/src/Pages/Tags/components/TagsTable.tsx @@ -1,7 +1,7 @@ import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu"; import Typography from "@mui/material/Typography"; import type { Header } from "@/Components/design-elements/Table"; -import { Table, Chip } from "@/Components/design-elements"; +import { Table, Chip, ColoredLabel } from "@/Components/design-elements"; import { Pagination } from "@/Components/design-elements/Table"; import { useClientPagination } from "@/Hooks/useClientPagination"; @@ -66,10 +66,10 @@ export const TagsTable = ({ tags, setSelectedTag }: TagsTableProps) => { }, { id: "chip", - content: t("pages.tags.table.headers.chip"), + content: t("pages.tags.table.headers.appearance"), render: (row) => { return ( - From f37ae8fddbf093986f75a097f391c3a043742235 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:45:55 -0700 Subject: [PATCH 10/23] add menu string --- client/src/locales/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 0428a76d4..becf25a38 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -184,6 +184,7 @@ "pagespeed": "Pagespeed", "infrastructure": "Infrastructure", "notifications": "Notifications", + "tags": "Tags", "checks": "Checks", "incidents": "Incidents", "statusPages": "Status pages", @@ -889,7 +890,7 @@ "table": { "headers": { "color": "Color", - "chip": "Chip" + "appearance": "Appearance" } }, "form": { From b981427d8d42de6d4588931566827f9d7b93263d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 10:49:16 -0700 Subject: [PATCH 11/23] mutate SWR on success --- client/src/Pages/Tags/create/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/Tags/create/index.tsx b/client/src/Pages/Tags/create/index.tsx index 9ce7ca7c4..5e29096d0 100644 --- a/client/src/Pages/Tags/create/index.tsx +++ b/client/src/Pages/Tags/create/index.tsx @@ -8,6 +8,7 @@ import { useParams } from "react-router-dom"; import { useNavigate } from "react-router-dom"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { mutate } from "swr"; import { useGet, usePost, usePatch } from "@/Hooks/UseApi"; import type { TagFormData } from "@/Validation/tag"; import type { Tag } from "@/Types/Tag"; @@ -43,7 +44,8 @@ const TagsCreatePage = () => { const result = isEditMode ? await patch(`/tags/${tagId}`, data) : await post("/tags", data); - if (result) { + if (result?.success) { + await mutate((key) => typeof key === "string" && key.startsWith("/tags")); navigate("/tags"); } }; From 67bba1479c56166940fd6371762201711b168d45 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 11:36:03 -0700 Subject: [PATCH 12/23] remove chip, add coloredlabel, add render options to autocomplete --- .../src/Components/design-elements/Chip.tsx | 30 ------------- .../design-elements/StatusLabel.tsx | 5 +-- client/src/Components/inputs/AutoComplete.tsx | 44 +++++++++++-------- 3 files changed, 26 insertions(+), 53 deletions(-) delete mode 100644 client/src/Components/design-elements/Chip.tsx diff --git a/client/src/Components/design-elements/Chip.tsx b/client/src/Components/design-elements/Chip.tsx deleted file mode 100644 index c5a902feb..000000000 --- a/client/src/Components/design-elements/Chip.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import MuiChip from "@mui/material/Chip"; -import Typography from "@mui/material/Typography"; -import { useTheme } from "@mui/material"; -interface ChipProps { - text: string; - color?: string; -} - -export const Chip = ({ text, color }: ChipProps) => { - const theme = useTheme(); - const textColor = color - ? theme.palette.getContrastText(color) - : theme.palette.text.primary; - return ( - - {text} - - } - sx={{ - backgroundColor: color, - }} - /> - ); -}; diff --git a/client/src/Components/design-elements/StatusLabel.tsx b/client/src/Components/design-elements/StatusLabel.tsx index ee9a749c2..c3fd089d7 100644 --- a/client/src/Components/design-elements/StatusLabel.tsx +++ b/client/src/Components/design-elements/StatusLabel.tsx @@ -100,10 +100,7 @@ export const ColoredLabel = ({ text, color }: { text: string; color: string }) = return ( ["renderInput"]; fieldLabel?: string; required?: boolean; + renderOptionContent?: (option: any) => React.ReactNode; }; export const AutoCompleteInput = ({ fieldLabel, required, renderInput, + renderOptionContent, ...props }: AutoCompleteInputProps) => { const theme = useTheme(); const multiple = props.multiple; - + const { renderOption: customRenderOption, ...autocompleteProps } = props; const defaultRenderInput = (params: any) => ( option.id} renderTags={() => null} - renderOption={(props, option, { selected }) => { - const { key, ...optionProps } = props; - return ( - - { + const { key, ...optionProps } = props; + return ( + - {multiple && } - {option.name} - - - ); - }} + + {multiple && } + {renderOptionContent ? renderOptionContent(option) : option.name} + + + ); + }) + } sx={{ "&.MuiAutocomplete-root .MuiAutocomplete-input": { padding: `0 ${theme.spacing(5)}`, From 76953f9a5c954d302458668c362aceb3d11921b7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 11:36:23 -0700 Subject: [PATCH 13/23] add tags to monitor forms --- client/src/Hooks/useMonitorForm.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 990768b96..eeb5c138e 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -13,6 +13,7 @@ const getBaseDefaults = (data?: Monitor | null) => ({ description: data?.description || "", interval: data?.interval || 60000, notifications: data?.notifications || [], + tags: data?.tags || [], statusWindowSize: data?.statusWindowSize || 5, statusWindowThreshold: data?.statusWindowThreshold || 60, geoCheckEnabled: data?.geoCheckEnabled ?? false, From a09a8857865ee7b3938e760d55dd334797d4a8d1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 11:36:40 -0700 Subject: [PATCH 14/23] add tags to monitor type --- client/src/Types/Monitor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 02c434fea..1e3dafb99 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -70,6 +70,7 @@ export interface Monitor { interval: number; uptimePercentage?: number; notifications: string[]; + tags: string[]; secret?: string; cpuAlertThreshold: number; cpuAlertCounter: number; From 7d7926fab9330709fa4cfc8aca7ccdf087c1293d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 11:36:46 -0700 Subject: [PATCH 15/23] add tags to validation --- client/src/Validation/monitor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index adbe64e4a..17be250b1 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -14,6 +14,7 @@ const baseSchema = z.object({ description: z.string().optional(), interval: z.number().min(15000, "Interval must be at least 15 seconds"), notifications: z.array(z.string()), + tags: z.array(z.string()), statusWindowSize: z .number({ message: "Status window size is required" }) .min(1, "Status window size must be at least 1") From 3f290bc963f5a15c95c1c5fd5c2b5066f2807423 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 11:37:03 -0700 Subject: [PATCH 16/23] add tags to Uptime montitor table --- client/src/Pages/CreateMonitor/index.tsx | 76 ++++++++++++++++++- .../Components/UptimeMonitorsTable.tsx | 34 +++++++++ client/src/Pages/Uptime/Monitors/index.tsx | 4 + 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 235ac95bf..3aae2e5e1 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -18,7 +18,7 @@ import { Trash2 } from "lucide-react"; import { HeaderDeleteControls } from "@/Components/monitors"; import { GeoContinents } from "@/Types/GeoCheck"; -import { BasePage, ConfigBox } from "@/Components/design-elements"; +import { BasePage, ColoredLabel, ConfigBox } from "@/Components/design-elements"; import { RadioWithDescription, Button, @@ -40,6 +40,7 @@ import { DefaultPageSpeedStrategy, } from "@/Types/Monitor"; import type { Notification } from "@/Types/Notification"; +import type { Tag } from "@/Types/Tag"; import type { MonitorFormData } from "@/Validation/monitor"; interface GeneralSettingsConfig { @@ -237,6 +238,7 @@ const CreateMonitorPage = () => { const { data: notifications } = useGet("/notifications/team"); const { data: games } = useGet("/monitors/games"); + const { data: tags } = useGet("/tags/team"); const { schema, defaults } = useMonitorForm({ data: existingMonitor ?? null, @@ -923,6 +925,78 @@ const CreateMonitorPage = () => { } /> + { + const tagOptions = tags ?? []; + const selectedTags = tagOptions.filter((tag) => + (field.value ?? []).includes(tag.id) + ); + return ( + + option.name} + onChange={(_: unknown, newValue: typeof tagOptions) => { + field.onChange(newValue.map((tag) => tag.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + renderOptionContent={(option) => ( + + )} + /> + {selectedTags.length > 0 && ( + + {selectedTags.map((tag, index) => ( + + +
+ { + field.onChange( + (field.value ?? []).filter((id: string) => id !== tag.id) + ); + }} + aria-label="Remove tag" + > + + + {index < selectedTags.length - 1 && } + + ))} + + )} + + ); + }} + /> + } + /> + {(watchedType === "http" || watchedType === "grpc" || watchedType === "websocket") && ( diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index 048998776..a9344f4ce 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -6,6 +6,7 @@ import { type Header, Pagination, StatusLabel, + ColoredLabel, } from "@/Components/design-elements"; import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/common"; import { ActionsMenu } from "@/Components/actions-menu"; @@ -16,14 +17,18 @@ import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; import { usePost } from "@/Hooks/UseApi"; import { useSelector } from "react-redux"; +import useMediaQuery from "@mui/material/useMediaQuery"; import type { Monitor } from "@/Types/Monitor"; import type { ActionMenuItem } from "@/Components/actions-menu"; import type { RootState } from "@/Types/state"; import { Checkbox } from "@/Components/inputs"; +import type { Tag } from "@/Types/Tag"; +import { SPACING } from "@/Utils/Theme/constants"; interface MonitorTableProps { monitors: Monitor[]; + tags: Tag[]; refetch: () => void; setSelectedMonitor: (monitor: Monitor | null) => void; sortField: string; @@ -41,6 +46,7 @@ interface MonitorTableProps { export const MonitorTable = ({ monitors, + tags, refetch, setSelectedMonitor, sortField, @@ -60,6 +66,7 @@ export const MonitorTable = ({ const navigate = useNavigate(); const chartType = useSelector((state: RootState) => state.ui?.chartType ?? "histogram"); const { post } = usePost, Monitor>(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); const selectedSet = new Set(selectedRows); const isAllSelected = @@ -252,6 +259,33 @@ export const MonitorTable = ({ } }, }, + { + id: "tags", + content: t("pages.uptime.table.headers.responseTime"), + render: (row) => { + if (row.tags.length === 0) return "-"; + + return ( + + {row.tags.map((tag) => { + const fullTag = tags.find((t) => t.id === tag); + if (!fullTag) return null; + return ( + + ); + })} + + ); + }, + }, { id: "type", mobileLabel: t("common.table.headers.type"), diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 85db2cb11..73dbfa511 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useGet, useDelete } from "@/Hooks/UseApi"; import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor"; +import type { Tag } from "@/Types/Tag"; import { useState, useMemo, useCallback } from "react"; import { useSelector, useDispatch } from "react-redux"; import { setRowsPerPage } from "@/Features/UI/uiSlice.js"; @@ -103,6 +104,8 @@ const UptimeMonitorsPage = () => { { refreshInterval: 5000, keepPreviousData: true } ); + const { data: tags } = useGet("/tags/team"); + const { monitors: monitorsWithChecks, summary, @@ -218,6 +221,7 @@ const UptimeMonitorsPage = () => { Date: Wed, 20 May 2026 11:39:57 -0700 Subject: [PATCH 17/23] remove chip referecnes, update strings --- client/src/Components/design-elements/index.tsx | 1 - client/src/Pages/Tags/components/TagsTable.tsx | 2 +- .../Uptime/Monitors/Components/UptimeMonitorsTable.tsx | 2 +- client/src/locales/en.json | 7 ++++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/src/Components/design-elements/index.tsx b/client/src/Components/design-elements/index.tsx index 0954041cd..130ccd831 100644 --- a/client/src/Components/design-elements/index.tsx +++ b/client/src/Components/design-elements/index.tsx @@ -27,4 +27,3 @@ export * from "./OfflineBanner"; export * from "./Avatar"; export * from "./StatusCodeLabel"; export * from "./StrategyBadge"; -export * from "./Chip"; diff --git a/client/src/Pages/Tags/components/TagsTable.tsx b/client/src/Pages/Tags/components/TagsTable.tsx index a84922140..00d7802ec 100644 --- a/client/src/Pages/Tags/components/TagsTable.tsx +++ b/client/src/Pages/Tags/components/TagsTable.tsx @@ -1,7 +1,7 @@ import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu"; import Typography from "@mui/material/Typography"; import type { Header } from "@/Components/design-elements/Table"; -import { Table, Chip, ColoredLabel } from "@/Components/design-elements"; +import { Table, ColoredLabel } from "@/Components/design-elements"; import { Pagination } from "@/Components/design-elements/Table"; import { useClientPagination } from "@/Hooks/useClientPagination"; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index a9344f4ce..2c026c35d 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -261,7 +261,7 @@ export const MonitorTable = ({ }, { id: "tags", - content: t("pages.uptime.table.headers.responseTime"), + content: t("common.table.headers.tags"), render: (row) => { if (row.tags.length === 0) return "-"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index becf25a38..8e79e23b3 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -131,7 +131,8 @@ "url": "Url", "interval": "Interval", "active": "Active", - "responseTime": "Response time" + "responseTime": "Response time", + "tags": "Tags" } } }, @@ -628,6 +629,10 @@ "description": "Select the notification channels you want to use", "title": "Notifications" }, + "tags": { + "description": "Select the tags you want to apply to this monitor", + "title": "Tags" + }, "type": { "description": "Select the type of check to perform", "optionDockerDescription": "Use Docker to monitor if a container is running.", From c6d82223f9523c1359e5a7386b5fadd4abcf8f95 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 May 2026 12:02:32 -0700 Subject: [PATCH 18/23] add tags to filters --- .../Components/monitors/ControlsFilter.tsx | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/client/src/Components/monitors/ControlsFilter.tsx b/client/src/Components/monitors/ControlsFilter.tsx index a617d454c..6cb60c935 100644 --- a/client/src/Components/monitors/ControlsFilter.tsx +++ b/client/src/Components/monitors/ControlsFilter.tsx @@ -1,9 +1,11 @@ import Stack from "@mui/material/Stack"; -import { Select, Button } from "@/Components/inputs"; +import { Select, Autocomplete, Button, TextField } from "@/Components/inputs"; +import { ColoredLabel } from "@/Components/design-elements"; import MenuItem from "@mui/material/MenuItem"; import useMediaQuery from "@mui/material/useMediaQuery"; import type { MonitorType } from "@/Types/Monitor"; +import type { Tag } from "@/Types/Tag"; import { Typography, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; @@ -28,6 +30,9 @@ export const ControlsFilter = ({ setSelectedStatus, selectedState, setSelectedState, + tagOptions, + selectedTags, + setSelectedTags, onClearFilters, }: { showTypes?: boolean; @@ -37,13 +42,22 @@ export const ControlsFilter = ({ setSelectedStatus: React.Dispatch>; selectedState: string; setSelectedState: React.Dispatch>; + tagOptions?: Tag[]; + selectedTags?: string[]; + setSelectedTags?: React.Dispatch>; onClearFilters: () => void; }) => { const theme = useTheme(); const { t } = useTranslation(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isFilterActive = - (selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== ""; + (selectedTypes?.length ?? 0) > 0 || + selectedStatus !== "" || + selectedState !== "" || + (selectedTags?.length ?? 0) > 0; + const selectedTagObjects = (tagOptions ?? []).filter((tag) => + (selectedTags ?? []).includes(tag.id) + ); return ( ))} + {tagOptions && setSelectedTags && ( + option.name} + onChange={(_: unknown, newValue: Tag[]) => { + setSelectedTags(newValue.map((tag) => tag.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + renderOptionContent={(option) => ( + + )} + renderInput={(params) => ( + + )} + slotProps={{ + popper: { + sx: { width: "fit-content !important", minWidth: 280 }, + }, + }} + sx={{ minWidth: 180 }} + /> + )} {isFilterActive && (