From f1dc058bdb8284f38d875f20451e1b270b81318b Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:09:36 +0100 Subject: [PATCH 01/12] Clickable modal --- web/messages/en/modal.json | 4 ++ .../SettingsActivityStreamingTab.tsx | 45 +++++++++++++ .../AddLogStreamingModal.tsx | 67 +++++++++++++++++++ .../modals/AddDestinationModal/style.scss | 0 .../useAddDestinationModal.tsx | 31 +++++++++ .../style.scss | 5 ++ .../SettingsIndexPage/SettingsIndexPage.tsx | 5 +- web/src/pages/settings/shared/consts.ts | 7 ++ web/src/shared/defguard-ui | 2 +- .../shared/hooks/modalControls/modalTypes.ts | 4 ++ 10 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx create mode 100644 web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/AddLogStreamingModal.tsx create mode 100644 web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/style.scss create mode 100644 web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/useAddDestinationModal.tsx create mode 100644 web/src/pages/settings/SettingsActivityLogStreamingPage/style.scss diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index 21237ecf70..56864af12d 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -74,12 +74,16 @@ "modal_add_api_token_copy_warning": "Please copy and save the API token below now. You won't be able to see it again.", "modal_rename_api_title": "Rename API token", "modal_add_user_title": "Add new user", + "modal_add_vector_destination_title": "Add Vector destination", + "modal_add_logstash_destination_title": "Add Logstash destination", "modal_add_user_groups_title": "Assign user to group(s)", "modal_add_user_enroll_title": "Start enrollment for user", "modal_add_user_choice_enroll_title": "Add user with self-enrollment option", "modal_add_user_choice_enroll_content": "User will be able to configure their account during the setup of their desktop client (e.g., set their own password, etc.).", "modal_add_user_choice_manual_title": "Add user manually", "modal_add_user_choice_manual_content": "The admin creates the user account by providing the details and assigning a password on their behalf.", + "modal_add_logstash_destination": "Logstash is an activity log provider that collects, processes, and streams event data from multiple sources in real time.", + "modal_add_vector_destination": "Vector is a high-performance observability data pipeline that collects, transforms, and routes logs, metrics, and events.", "modal_add_user_form_enable_enroll_label": "Enable user secure remote self-enrollment.", "modal_add_user_form_enable_enroll_help": "By enabling this option, the user will be able to configure their account during the setup of their desktop client (e.g., set their own password, etc.). ", "modal_add_user_submit": "Add user", diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx b/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx new file mode 100644 index 0000000000..45b96cecd3 --- /dev/null +++ b/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; +import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; +import { EmptyState } from '../../../shared/defguard-ui/components/EmptyState/EmptyState'; +import { businessPlanBadgeProps } from '../shared/consts'; +import './style.scss'; +import type { ButtonProps } from '../../../shared/defguard-ui/components/Button/types'; +import { openModal } from '../../../shared/hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../../shared/hooks/modalControls/modalTypes'; +import { AddLogStreamingModal } from './modals/AddDestinationModal/AddLogStreamingModal'; + +export const SettingsActivityLogStreamingPage = () => { + const isEmpty = false; + + const addButtonProps = useMemo( + (): ButtonProps => ({ + text: 'Add log streaming', + iconLeft: 'file-add', + testId: 'add-activity-stream', + onClick: () => { + openModal(ModalName.AddLogStreaming); + }, + }), + [], + ); + + return ( + + + + + + ); +}; diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/AddLogStreamingModal.tsx b/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/AddLogStreamingModal.tsx new file mode 100644 index 0000000000..4972f68e05 --- /dev/null +++ b/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/AddLogStreamingModal.tsx @@ -0,0 +1,67 @@ +import { ModalName } from '../../../../../shared/hooks/modalControls/modalTypes'; +import { useEffect, useState } from 'react'; +import { + subscribeCloseModal, + subscribeOpenModal, +} from '../../../../../shared/hooks/modalControls/modalsSubjects'; +import { Modal } from '../../../../../shared/defguard-ui/components/Modal/Modal'; +import { SectionSelect } from '../../../../../shared/defguard-ui/components/SectionSelect/SectionSelect'; +import { useAddDestinationModal } from './useAddDestinationModal'; +import { SizedBox } from '../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { ThemeSpacing } from '../../../../../shared/defguard-ui/types'; +import { m } from '../../../../../paraglide/messages'; + +const modalNameValue = ModalName.AddLogStreaming; + +export const AddLogStreamingModal = () => { + const [isOpen, setOpen] = useState(false); + + useEffect(() => { + const openSub = subscribeOpenModal(modalNameValue, () => { + setOpen(true); + }); + const closeSub = subscribeCloseModal(modalNameValue, () => setOpen(false)); + return () => { + openSub.unsubscribe(); + closeSub.unsubscribe(); + }; + }, []); + + return ( + setOpen(false)}> + + + ); +}; + +const ModalContent = () => { + return ( + <> + { + // useAddUserModal.setState({ + // step: 'user', + // enrollUser: false, + // }); + useAddDestinationModal.setState({ + destination: 'logstash', + }); + }} + /> + + { + //todo + }} + /> + + ); +}; diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/style.scss b/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/style.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/useAddDestinationModal.tsx b/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/useAddDestinationModal.tsx new file mode 100644 index 0000000000..a745b93592 --- /dev/null +++ b/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/useAddDestinationModal.tsx @@ -0,0 +1,31 @@ +import { create } from 'zustand'; + +interface StoreValues { + step: 'choice' | 'destination'; + destination?: 'logstash' | 'vector'; + isOpen: boolean; + name: string; + url: string; + username?: string; + password?: string; + certificate?: string; +} + +const defaults: StoreValues = { + isOpen: false, + step: 'choice', + name: '', + url: '', + username: '', + password: '', + certificate: '', +}; + +interface Store extends StoreValues { + reset: () => void; +} + +export const useAddDestinationModal = create((set) => ({ + ...defaults, + reset: () => set(defaults), +})); diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/style.scss b/web/src/pages/settings/SettingsActivityLogStreamingPage/style.scss new file mode 100644 index 0000000000..664b1516ff --- /dev/null +++ b/web/src/pages/settings/SettingsActivityLogStreamingPage/style.scss @@ -0,0 +1,5 @@ +#settings-activity-log-streaming-tab { + .empty-state { + margin-top: 120px; + } +} diff --git a/web/src/pages/settings/SettingsIndexPage/SettingsIndexPage.tsx b/web/src/pages/settings/SettingsIndexPage/SettingsIndexPage.tsx index 014e22a31a..6c1d64b3a8 100644 --- a/web/src/pages/settings/SettingsIndexPage/SettingsIndexPage.tsx +++ b/web/src/pages/settings/SettingsIndexPage/SettingsIndexPage.tsx @@ -10,14 +10,13 @@ import { SettingsGeneralTab } from './tabs/SettingsGeneralTab'; import { SettingsLicenseTab } from './tabs/SettingsLicenseTab/SettingsLicenseTab'; import { SettingsNotificationsTab } from './tabs/SettingsNotificationsTab'; import { type SettingsTabValue, settingsTabsSchema } from './types'; - -const ActivityTab = () => null; +import { SettingsActivityLogStreamingPage } from '../SettingsActivityLogStreamingPage/SettingsActivityStreamingTab'; const tabComponent: Record = { general: , notifications: , openid: , - activity: , + activity: , license: , }; diff --git a/web/src/pages/settings/shared/consts.ts b/web/src/pages/settings/shared/consts.ts index 2c910c5ce3..b6098b1a27 100644 --- a/web/src/pages/settings/shared/consts.ts +++ b/web/src/pages/settings/shared/consts.ts @@ -6,3 +6,10 @@ export const higherPlanBadgeProps: BadgeProps = { showIcon: true, variant: 'warning', }; + +export const businessPlanBadgeProps: BadgeProps = { + text: 'Business feature', + icon: 'status-premium', + showIcon: true, + variant: 'additional', +}; diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 64bbbc159b..fa1f515221 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 64bbbc159b9e8c4bbb4ff7b4f08f40690708a1fb +Subproject commit fa1f515221717eb816b4df67cfaaef73cd02afa9 diff --git a/web/src/shared/hooks/modalControls/modalTypes.ts b/web/src/shared/hooks/modalControls/modalTypes.ts index 815d4a3e17..6472cd3c43 100644 --- a/web/src/shared/hooks/modalControls/modalTypes.ts +++ b/web/src/shared/hooks/modalControls/modalTypes.ts @@ -45,6 +45,7 @@ export const ModalName = { NetworkDeviceConfig: 'networkDeviceConfig', NetworkDeviceToken: 'networkDeviceToken', AddLocation: 'addLocation', + AddLogStreaming: 'addLogStreaming', } as const; export type ModalNameValue = (typeof ModalName)[keyof typeof ModalName]; @@ -138,6 +139,9 @@ const modalOpenArgsSchema = z.discriminatedUnion('name', [ z.object({ name: z.literal(ModalName.AddLocation), }), + z.object({ + name: z.literal(ModalName.AddLogStreaming), + }), z.object({ name: z.literal(ModalName.SendTestMail), }), From 0db42868bc4ea8b064097d5f54fbc3ebe9abd462 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 14 Jan 2026 20:15:10 +0100 Subject: [PATCH 02/12] creating log stream --- web/messages/en/common.json | 1 + .../SettingsActivityStreamingTab.tsx | 27 ++- .../AddLogStreamingModal.tsx | 167 ++++++++++++++++-- .../useAddDestinationModal.tsx | 15 +- web/src/shared/api/api.ts | 10 ++ web/src/shared/api/types.ts | 36 +++- 6 files changed, 218 insertions(+), 38 deletions(-) diff --git a/web/messages/en/common.json b/web/messages/en/common.json index 299403d007..95fc8c9d37 100644 --- a/web/messages/en/common.json +++ b/web/messages/en/common.json @@ -27,6 +27,7 @@ "controls_keep": "Keep", "controls_submit": "Submit", "controls_cancel": "Cancel", + "controls_add_destination": "Add destination", "controls_save": "Save", "controls_save_changes": "Save changes", "controls_sign_in": "Sign in", diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx b/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx index 45b96cecd3..9636d5887a 100644 --- a/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx +++ b/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx @@ -10,7 +10,7 @@ import { ModalName } from '../../../shared/hooks/modalControls/modalTypes'; import { AddLogStreamingModal } from './modals/AddDestinationModal/AddLogStreamingModal'; export const SettingsActivityLogStreamingPage = () => { - const isEmpty = false; + const isEmpty = true; const addButtonProps = useMemo( (): ButtonProps => ({ @@ -30,15 +30,24 @@ export const SettingsActivityLogStreamingPage = () => { badgeProps={businessPlanBadgeProps} icon="activity" title="Activity log streaming" - subtitle="Monitor and export real-time activity logs from your Defguard instance. Stream events to external systems for auditing, analytics, or security monitoring." - /> - + {isEmpty ? ( + + ) : ( + + )} ); diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/AddLogStreamingModal.tsx b/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/AddLogStreamingModal.tsx index 4972f68e05..7045f48331 100644 --- a/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/AddLogStreamingModal.tsx +++ b/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/AddLogStreamingModal.tsx @@ -1,5 +1,5 @@ import { ModalName } from '../../../../../shared/hooks/modalControls/modalTypes'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { subscribeCloseModal, subscribeOpenModal, @@ -10,17 +10,42 @@ import { useAddDestinationModal } from './useAddDestinationModal'; import { SizedBox } from '../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../../../shared/defguard-ui/types'; import { m } from '../../../../../paraglide/messages'; +import { useStore } from '@tanstack/react-form'; +import { useAppForm } from '../../../../../shared/form'; +import { formChangeLogic } from '../../../../../shared/formLogic'; +import z from 'zod'; +import { ModalControls } from '../../../../../shared/defguard-ui/components/ModalControls/ModalControls'; +import { Button } from '../../../../../shared/defguard-ui/components/Button/Button'; +import api from '../../../../../shared/api/api'; +import { + ActivityLogStreamType, + type CreateActivityLogStreamRequest, +} from '../../../../../shared/api/types'; const modalNameValue = ModalName.AddLogStreaming; export const AddLogStreamingModal = () => { - const [isOpen, setOpen] = useState(false); + const isOpen = useAddDestinationModal((s) => s.isOpen); + const step = useAddDestinationModal((s) => s.step); + const destination = useAddDestinationModal((s) => s.destination); + + const modalTitle = useMemo(() => { + if (step === 'choice') return 'Select destination'; + switch (destination) { + case 'logstash': + return 'Add Logstash destination'; + case 'vector': + return 'Add Vector destination'; + } + }, [step, destination]); useEffect(() => { const openSub = subscribeOpenModal(modalNameValue, () => { - setOpen(true); + useAddDestinationModal.setState({ isOpen: true }); + }); + const closeSub = subscribeCloseModal(modalNameValue, () => { + useAddDestinationModal.setState({ isOpen: false }); }); - const closeSub = subscribeCloseModal(modalNameValue, () => setOpen(false)); return () => { openSub.unsubscribe(); closeSub.unsubscribe(); @@ -28,13 +53,19 @@ export const AddLogStreamingModal = () => { }, []); return ( - setOpen(false)}> - + useAddDestinationModal.setState({ isOpen: false })} + afterClose={() => useAddDestinationModal.getState().reset()} + > + {step === 'choice' && } + {step === 'form' && } ); }; -const ModalContent = () => { +const ChoiceStep = () => { return ( <> { content={m.modal_add_logstash_destination()} data-testid="add-logstash" onClick={() => { - // useAddUserModal.setState({ - // step: 'user', - // enrollUser: false, - // }); useAddDestinationModal.setState({ + step: 'form', destination: 'logstash', }); }} @@ -59,9 +87,124 @@ const ModalContent = () => { title="Vector" data-testid="add-vector" onClick={() => { - //todo + useAddDestinationModal.setState({ + step: 'form', + destination: 'vector', + }); }} /> ); }; + +const FormStep = () => { + const destination = useAddDestinationModal((s) => s.destination); + + const formSchema = useMemo( + () => + z.object({ + name: z.string().trim().min(1, m.form_error_required()), + url: z.string().trim().min(1, m.form_error_required()), + username: z.string().optional(), + password: z.string().optional(), + certificate: z.string().optional(), + }), + [], + ); + + type FormFields = z.infer; + + const defaultValues = useMemo( + (): FormFields => ({ + name: '', + url: '', + username: '', + password: '', + certificate: '', + }), + [], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: formSchema, + onChange: formSchema, + }, + onSubmit: async ({ value }) => { + const requestData: CreateActivityLogStreamRequest = { + name: value.name, + stream_type: + destination === 'logstash' + ? ActivityLogStreamType.LogstashHttp + : ActivityLogStreamType.VectorHttp, + stream_config: { + url: value.url, + username: value.username || undefined, + password: value.password || undefined, + cert: value.certificate || undefined, + }, + }; + + await api.activityLogStream.createStream(requestData); + useAddDestinationModal.setState({ isOpen: false }); + }, + }); + + return ( + <> +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + + + {(field) => } + + + + + + {(field) => } + + + + + + {(field) => } + + + + + + {(field) => } + + + + + + {(field) => } + + +
+ + form.handleSubmit(), + }} + cancelProps={{ + text: m.controls_cancel(), + onClick: () => { + useAddDestinationModal.setState({ isOpen: false }); + }, + }} + > + + ); +}; diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/useAddDestinationModal.tsx b/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/useAddDestinationModal.tsx index a745b93592..197e1d859d 100644 --- a/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/useAddDestinationModal.tsx +++ b/web/src/pages/settings/SettingsActivityLogStreamingPage/modals/AddDestinationModal/useAddDestinationModal.tsx @@ -1,24 +1,15 @@ import { create } from 'zustand'; interface StoreValues { - step: 'choice' | 'destination'; - destination?: 'logstash' | 'vector'; + step: 'choice' | 'form'; + destination: 'logstash' | 'vector'; isOpen: boolean; - name: string; - url: string; - username?: string; - password?: string; - certificate?: string; } const defaults: StoreValues = { isOpen: false, + destination: 'logstash', step: 'choice', - name: '', - url: '', - username: '', - password: '', - certificate: '', }; interface Store extends StoreValues { diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index 04430a1158..7f402a3bcf 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -7,6 +7,7 @@ import type { AclRule, ActivityLogEvent, ActivityLogRequestParams, + ActivityLogStream, AddAclAliasRequest, AddAclRuleRequest, AddApiTokenRequest, @@ -29,6 +30,7 @@ import type { AvailableLocationIpResponse, ChangeAccountActiveRequest, ChangeWebhookStateRequest, + CreateActivityLogStreamRequest, CreateGroupRequest, DeleteApiTokenRequest, DeleteAuthKeyRequest, @@ -376,6 +378,14 @@ const api = { deleteRule: (ruleId: number | string) => client.delete(`/acl/rule/${ruleId}`), }, }, + activityLogStream: { + getStreams: () => client.get('/activity_log_stream'), + createStream: (data: CreateActivityLogStreamRequest) => + client.post('/activity_log_stream', data), + updateStream: (id: number, data: CreateActivityLogStreamRequest) => + client.put(`/activity_log_stream/${id}`, data), + deleteStream: (id: number) => client.delete(`/activity_log_stream/${id}`), + }, getActivityLog: (data?: ActivityLogRequestParams) => client .get>(`/activity_log`, { diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 029e7f1385..5136e9b563 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -493,11 +493,10 @@ export interface NetworkLocation { service_location_mode: LocationServiceModeValue; } -export interface EditNetworkLocation - extends Omit< - NetworkLocation, - 'gateways' | 'connected_at' | 'id' | 'connected' | 'allowed_ips' | 'address' - > { +export interface EditNetworkLocation extends Omit< + NetworkLocation, + 'gateways' | 'connected_at' | 'id' | 'connected' | 'allowed_ips' | 'address' +> { allowed_ips: string; address: string; } @@ -856,6 +855,33 @@ export interface ActivityLogEvent { description?: string; } +export const ActivityLogStreamType = { + VectorHttp: 'vector_http', + LogstashHttp: 'logstash_http', +} as const; + +export type ActivityLogStreamTypeValue = + (typeof ActivityLogStreamType)[keyof typeof ActivityLogStreamType]; + +export interface ActivityLogStream { + id: number; + name: string; + stream_type: ActivityLogStreamTypeValue; + config: ActivityLogStreamConfig; +} +export interface CreateActivityLogStreamRequest { + name: string; + stream_type: ActivityLogStreamTypeValue; + stream_config: ActivityLogStreamConfig; +} + +export interface ActivityLogStreamConfig { + url: string; + username?: string; + password?: string; + cert?: string; +} + export type ActivityLogSortKey = | 'timestamp' | 'username' From 66064ff016dde003e4e1a6cc773b6cca306fc443 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:59:17 +0100 Subject: [PATCH 03/12] log streaming table --- web/src/pages/AliasesPage/AliasTable.tsx | 1 + .../ActivityLogStreamTable.tsx | 114 ++++++++++++++++++ .../SettingsActivityStreamingTab.tsx | 23 ++-- .../style.scss | 11 ++ web/src/shared/query.ts | 6 + 5 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 web/src/pages/settings/SettingsActivityLogStreamingPage/ActivityLogStreamTable.tsx diff --git a/web/src/pages/AliasesPage/AliasTable.tsx b/web/src/pages/AliasesPage/AliasTable.tsx index bbfcc1af7b..ab4b97da89 100644 --- a/web/src/pages/AliasesPage/AliasTable.tsx +++ b/web/src/pages/AliasesPage/AliasTable.tsx @@ -177,6 +177,7 @@ export const AliasTable = ({ data: rowData }: Props) => { enableRowSelection: false, enableExpanding: false, enableSorting: true, + columnResizeMode: 'onChange', getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), }); diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/ActivityLogStreamTable.tsx b/web/src/pages/settings/SettingsActivityLogStreamingPage/ActivityLogStreamTable.tsx new file mode 100644 index 0000000000..bca8fff689 --- /dev/null +++ b/web/src/pages/settings/SettingsActivityLogStreamingPage/ActivityLogStreamTable.tsx @@ -0,0 +1,114 @@ +import { useMutation } from '@tanstack/react-query'; +import { + createColumnHelper, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { useMemo } from 'react'; +import { m } from '../../../paraglide/messages'; +import api from '../../../shared/api/api'; +import type { ActivityLogStream } from '../../../shared/api/types'; +import { IconButtonMenu } from '../../../shared/defguard-ui/components/IconButtonMenu/IconButtonMenu'; +import type { MenuItemsGroup } from '../../../shared/defguard-ui/components/Menu/types'; +import { tableEditColumnSize } from '../../../shared/defguard-ui/components/table/consts'; +import { TableBody } from '../../../shared/defguard-ui/components/table/TableBody/TableBody'; +import { TableCell } from '../../../shared/defguard-ui/components/table/TableCell/TableCell'; +import { openModal } from '../../../shared/hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../../shared/hooks/modalControls/modalTypes'; + +type RowData = ActivityLogStream; + +const columnHelper = createColumnHelper(); + +type Props = { + data: RowData[]; +}; + +export const ActivityLogStreamTable = ({ data: rowData }: Props) => { + const { mutate: deleteStream } = useMutation({ + mutationFn: api.activityLogStream.deleteStream, //TODO + meta: { + invalidate: ['activity_log_stream'], + }, + }); + + const columns = useMemo( + () => [ + columnHelper.accessor('name', { + header: 'Name', + minSize: 484, + enableSorting: true, + sortingFn: 'text', + cell: (info) => ( + + {info.getValue()} + + ), + }), + columnHelper.accessor('stream_type', { + header: 'Destination', + size: 200, + minSize: 100, + cell: (info) => { + const value = info.getValue(); + const displayValue = value === 'vector_http' ? 'Vector' : 'Logstash'; + return ( + + {displayValue} + + ); + }, + }), + columnHelper.display({ + id: 'edit', + enableResizing: false, + header: '', + enableSorting: false, + size: tableEditColumnSize, + cell: (info) => { + const row = info.row.original; + const menuItems: MenuItemsGroup[] = [ + { + items: [ + { + text: m.controls_edit(), + icon: 'edit', + onClick: () => { + // TODO: edit modal + openModal(ModalName.AddLogStreaming); + }, + }, + { + text: m.controls_delete(), + icon: 'delete', + variant: 'danger', + onClick: () => { + deleteStream(row.id); + }, + }, + ], + }, + ]; + return ( + + + + ); + }, + }), + ], + [deleteStream], + ); + + const table = useReactTable({ + columns, + data: rowData, + enableRowSelection: false, + columnResizeMode: 'onChange', + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + return ; +}; diff --git a/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx b/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx index 9636d5887a..6fbd6b9f32 100644 --- a/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx +++ b/web/src/pages/settings/SettingsActivityLogStreamingPage/SettingsActivityStreamingTab.tsx @@ -1,3 +1,4 @@ +import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; @@ -8,9 +9,15 @@ import type { ButtonProps } from '../../../shared/defguard-ui/components/Button/ import { openModal } from '../../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../../shared/hooks/modalControls/modalTypes'; import { AddLogStreamingModal } from './modals/AddDestinationModal/AddLogStreamingModal'; +import { getActivityLogStreamsQueryOptions } from '../../../shared/query'; +import { ActivityLogStreamTable } from './ActivityLogStreamTable'; +import { Button } from '../../../shared/defguard-ui/components/Button/Button'; +import { TableTop } from '../../../shared/defguard-ui/components/table/TableTop/TableTop'; export const SettingsActivityLogStreamingPage = () => { - const isEmpty = true; + const { data: streams } = useQuery(getActivityLogStreamsQueryOptions); + + const isEmpty = !streams || streams.length === 0; const addButtonProps = useMemo( (): ButtonProps => ({ @@ -30,7 +37,7 @@ export const SettingsActivityLogStreamingPage = () => { badgeProps={businessPlanBadgeProps} icon="activity" title="Activity log streaming" - subtitle="Monitor and export real-time activity logs from your Defguard instance. Stream events to external systems for auditing, analytics, or security monitoring." // TODO: add this and rest to common.json + subtitle="Monitor and export real-time activity logs from your Defguard instance. Stream events to external systems for auditing, analytics, or security monitoring." /> {isEmpty ? ( { primaryAction={addButtonProps} /> ) : ( - + <> + +