From e15ea9a1fc7c7212f8ab9e41eae7b36237ef156a Mon Sep 17 00:00:00 2001 From: Ben Elferink Date: Thu, 7 Nov 2024 11:38:06 +0200 Subject: [PATCH] [GEN-1672]: add logic for "go to details" button of toast notifications (#1702) --- frontend/webapp/app/page.tsx | 4 +- frontend/webapp/components/index.ts | 1 + .../components/notification/toast-list.tsx | 116 ++++++++++++++++++ .../actions/action-drawer-container/index.tsx | 11 +- .../actions/choose-action-modal/index.tsx | 11 +- .../add-rule-modal/index.tsx | 29 ++--- .../rule-drawer-container/index.tsx | 11 +- .../overview/overview-data-flow/index.tsx | 4 +- .../choose-source-modal/index.tsx | 14 +-- .../choose-sources-body/index.tsx | 77 +++--------- .../webapp/hooks/actions/useActionCRUD.ts | 43 ++++--- .../webapp/hooks/actions/useActionFormData.ts | 15 ++- .../hooks/destinations/useDestinationCRUD.ts | 43 ++++--- .../useInstrumentationRuleCRUD.ts | 44 ++++--- .../useInstrumentationRuleFormData.ts | 13 +- frontend/webapp/hooks/useSSE.ts | 4 +- frontend/webapp/reuseable-components/index.ts | 2 - .../reuseable-components/modal/index.tsx | 9 +- .../reuseable-components/modal/styled.ts | 21 ---- .../notification-note/index.tsx | 31 ++--- .../reuseable-components/toast-list/index.tsx | 101 --------------- frontend/webapp/styles/styled.tsx | 21 +++- frontend/webapp/utils/constants/string.tsx | 24 ++-- .../utils/functions/extract-monitors.ts | 7 -- frontend/webapp/utils/functions/formatters.ts | 49 ++++++++ frontend/webapp/utils/functions/index.ts | 1 - 26 files changed, 370 insertions(+), 336 deletions(-) create mode 100644 frontend/webapp/components/notification/toast-list.tsx delete mode 100644 frontend/webapp/reuseable-components/modal/styled.ts delete mode 100644 frontend/webapp/reuseable-components/toast-list/index.tsx delete mode 100644 frontend/webapp/utils/functions/extract-monitors.ts diff --git a/frontend/webapp/app/page.tsx b/frontend/webapp/app/page.tsx index ceeeaeaaf..48151e064 100644 --- a/frontend/webapp/app/page.tsx +++ b/frontend/webapp/app/page.tsx @@ -1,9 +1,9 @@ 'use client'; import { useEffect } from 'react'; -import { ROUTES, CONFIG } from '@/utils'; import { useRouter } from 'next/navigation'; import { useConfig, useNotify } from '@/hooks'; import { Loader } from '@keyval-dev/design-system'; +import { ROUTES, CONFIG, NOTIFICATION } from '@/utils'; export default function App() { const router = useRouter(); @@ -13,7 +13,7 @@ export default function App() { useEffect(() => { if (error) { notify({ - type: 'error', + type: NOTIFICATION.ERROR, title: error.name, message: error.message, }); diff --git a/frontend/webapp/components/index.ts b/frontend/webapp/components/index.ts index dc76a8271..14f31b9fd 100644 --- a/frontend/webapp/components/index.ts +++ b/frontend/webapp/components/index.ts @@ -6,3 +6,4 @@ export * from './destinations'; export * from './main'; export * from './modals'; export * from './notification/notification-list'; // old +export * from './notification/toast-list'; // new \ No newline at end of file diff --git a/frontend/webapp/components/notification/toast-list.tsx b/frontend/webapp/components/notification/toast-list.tsx new file mode 100644 index 000000000..8a6a3dc54 --- /dev/null +++ b/frontend/webapp/components/notification/toast-list.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import styled from 'styled-components'; +import { NotificationNote } from '@/reuseable-components'; +import { Notification, OVERVIEW_ENTITY_TYPES } from '@/types'; +import { DrawerBaseItem, useDrawerStore, useNotificationStore } from '@/store'; +import { useActualDestination, useActualSources, useGetActions, useGetInstrumentationRules } from '@/hooks'; +import { getIdFromSseTarget } from '@/utils'; + +const Container = styled.div` + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 10000; + display: flex; + flex-direction: column; + gap: 6px; + min-width: 600px; +`; + +export const ToastList: React.FC = () => { + const { notifications } = useNotificationStore(); + + return ( + + {notifications + .filter(({ dismissed }) => !dismissed) + .map((notif) => ( + + ))} + + ); +}; + +const Toast: React.FC = ({ id, type, title, message, crdType, target }) => { + const { markAsDismissed, markAsSeen } = useNotificationStore(); + + const { actions } = useGetActions(); + const { sources } = useActualSources(); + const { destinations } = useActualDestination(); + const { instrumentationRules } = useGetInstrumentationRules(); + const setSelectedItem = useDrawerStore(({ setSelectedItem }) => setSelectedItem); + + const onClick = () => { + if (crdType && target) { + const drawerItem: Partial = {}; + + switch (crdType) { + case OVERVIEW_ENTITY_TYPES.RULE: + drawerItem['type'] = OVERVIEW_ENTITY_TYPES.RULE; + drawerItem['id'] = getIdFromSseTarget(target, OVERVIEW_ENTITY_TYPES.RULE); + drawerItem['item'] = instrumentationRules.find((item) => item.ruleId === drawerItem['id']); + break; + + case OVERVIEW_ENTITY_TYPES.SOURCE: + case 'InstrumentedApplication': + case 'InstrumentationInstance': + drawerItem['type'] = OVERVIEW_ENTITY_TYPES.SOURCE; + drawerItem['id'] = getIdFromSseTarget(target, OVERVIEW_ENTITY_TYPES.SOURCE); + drawerItem['item'] = sources.find( + (item) => + item.kind === drawerItem['id']?.['kind'] && + item.name === drawerItem['id']?.['name'] && + item.namespace === drawerItem['id']?.['namespace'] + ); + break; + + case OVERVIEW_ENTITY_TYPES.ACTION: + drawerItem['type'] = OVERVIEW_ENTITY_TYPES.ACTION; + drawerItem['id'] = getIdFromSseTarget(target, OVERVIEW_ENTITY_TYPES.ACTION); + drawerItem['item'] = actions.find((item) => item.id === drawerItem['id']); + break; + + case OVERVIEW_ENTITY_TYPES.DESTINATION: + case 'Destination': + drawerItem['type'] = OVERVIEW_ENTITY_TYPES.DESTINATION; + drawerItem['id'] = getIdFromSseTarget(target, OVERVIEW_ENTITY_TYPES.DESTINATION); + drawerItem['item'] = destinations.find((item) => item.id === drawerItem['id']); + break; + + default: + break; + } + + if (!!drawerItem.item) { + setSelectedItem(drawerItem as DrawerBaseItem); + } + } + + markAsSeen(id); + markAsDismissed(id); + }; + + const onClose = ({ asSeen }) => { + markAsDismissed(id); + if (asSeen) markAsSeen(id); + }; + + return ( + + ); +}; diff --git a/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx b/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx index 744e4257e..ff4f9068e 100644 --- a/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx @@ -5,15 +5,14 @@ import { useDrawerStore } from '@/store'; import { CardDetails } from '@/components'; import type { ActionDataParsed } from '@/types'; import { ChooseActionBody } from '../choose-action-body'; +import { useActionCRUD, useActionFormData } from '@/hooks'; import OverviewDrawer from '../../overview/overview-drawer'; import buildCardFromActionSpec from './build-card-from-action-spec'; -import { useActionCRUD, useActionFormData, useNotify } from '@/hooks'; import { ACTION_OPTIONS } from '../choose-action-modal/action-options'; interface Props {} const ActionDrawer: React.FC = () => { - const notify = useNotify(); const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); const [isEditing, setIsEditing] = useState(false); @@ -68,13 +67,7 @@ const ActionDrawer: React.FC = () => { }; const handleSave = async (newTitle: string) => { - if (!validateForm()) { - notify({ - type: 'error', - title: 'Update', - message: 'Required fields are missing!', - }); - } else { + if (validateForm({ withAlert: true })) { const payload = { ...formData, name: newTitle, diff --git a/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx b/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx index ed8fdad7a..37c4b9bd8 100644 --- a/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx +++ b/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx @@ -1,8 +1,9 @@ import { ChooseActionBody } from '../'; import React, { useMemo, useState } from 'react'; +import { CenterThis, ModalBody } from '@/styles'; import { useActionCRUD, useActionFormData } from '@/hooks/actions'; import { ACTION_OPTIONS, type ActionOption } from './action-options'; -import { AutocompleteInput, Modal, NavigationButtons, Divider, FadeLoader, SectionTitle, ModalContent, Center } from '@/reuseable-components'; +import { AutocompleteInput, Modal, NavigationButtons, Divider, FadeLoader, SectionTitle } from '@/reuseable-components'; interface AddActionModalProps { isOpen: boolean; @@ -50,7 +51,7 @@ export const AddActionModal: React.FC = ({ isOpen, onClose /> } > - + = ({ isOpen, onClose {loading ? ( -
+ -
+ ) : ( )} ) : null} -
+ ); }; diff --git a/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx b/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx index 6af49d440..af6787590 100644 --- a/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx +++ b/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx @@ -1,18 +1,9 @@ +import { CenterThis, ModalBody } from '@/styles'; import { ChooseRuleBody } from '../choose-rule-body'; import { RULE_OPTIONS, RuleOption } from './rule-options'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useInstrumentationRuleCRUD, useInstrumentationRuleFormData } from '@/hooks'; -import { - AutocompleteInput, - Center, - Divider, - FadeLoader, - Modal, - ModalContent, - NavigationButtons, - NotificationNote, - SectionTitle, -} from '@/reuseable-components'; +import { AutocompleteInput, Divider, FadeLoader, Modal, NavigationButtons, NotificationNote, SectionTitle } from '@/reuseable-components'; interface Props { isOpen: boolean; @@ -22,11 +13,7 @@ interface Props { export const AddRuleModal: React.FC = ({ isOpen, onClose }) => { const { formData, handleFormChange, resetFormData, validateForm } = useInstrumentationRuleFormData(); const { createInstrumentationRule, loading } = useInstrumentationRuleCRUD({ onSuccess: handleClose }); - const [selectedItem, setSelectedItem] = useState(undefined); - - useEffect(() => { - if (!selectedItem) handleSelect(RULE_OPTIONS[0]); - }, [selectedItem]); + const [selectedItem, setSelectedItem] = useState(RULE_OPTIONS[0]); const isFormOk = useMemo(() => !!selectedItem && validateForm(), [selectedItem, formData]); @@ -63,7 +50,7 @@ export const AddRuleModal: React.FC = ({ isOpen, onClose }) => { /> } > - + = ({ isOpen, onClose }) => { {loading ? ( -
+ -
+ ) : ( )} ) : null} -
+ ); }; diff --git a/frontend/webapp/containers/main/instrumentation-rules/rule-drawer-container/index.tsx b/frontend/webapp/containers/main/instrumentation-rules/rule-drawer-container/index.tsx index 55af46efb..02a7d4454 100644 --- a/frontend/webapp/containers/main/instrumentation-rules/rule-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/instrumentation-rules/rule-drawer-container/index.tsx @@ -8,12 +8,11 @@ import type { InstrumentationRuleSpec } from '@/types'; import OverviewDrawer from '../../overview/overview-drawer'; import { RULE_OPTIONS } from '../add-rule-modal/rule-options'; import buildCardFromRuleSpec from './build-card-from-rule-spec'; -import { useInstrumentationRuleCRUD, useInstrumentationRuleFormData, useNotify } from '@/hooks'; +import { useInstrumentationRuleCRUD, useInstrumentationRuleFormData } from '@/hooks'; interface Props {} const RuleDrawer: React.FC = () => { - const notify = useNotify(); const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); const [isEditing, setIsEditing] = useState(false); @@ -66,13 +65,7 @@ const RuleDrawer: React.FC = () => { }; const handleSave = async (newTitle: string) => { - if (!validateForm()) { - notify({ - type: 'error', - title: 'Update', - message: 'Required fields are missing!', - }); - } else { + if (validateForm({ withAlert: true })) { const payload = { ...formData, ruleName: newTitle, diff --git a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx index acdd98109..b5f7285e3 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx @@ -2,8 +2,9 @@ import React, { useMemo } from 'react'; import dynamic from 'next/dynamic'; import styled from 'styled-components'; +import { ToastList } from '@/components'; import { OverviewActionMenuContainer } from '../overview-actions-menu'; -import { buildNodesAndEdges, NodeBaseDataFlow, ToastList } from '@/reuseable-components'; +import { buildNodesAndEdges, NodeBaseDataFlow, } from '@/reuseable-components'; import { useMetrics, useGetActions, @@ -14,6 +15,7 @@ import { useGetInstrumentationRules, } from '@/hooks'; + const AllDrawers = dynamic(() => import('../all-drawers'), { ssr: false, }); diff --git a/frontend/webapp/containers/main/sources/choose-sources/choose-source-modal/index.tsx b/frontend/webapp/containers/main/sources/choose-sources/choose-source-modal/index.tsx index 1a9dcdecf..4e96a1097 100644 --- a/frontend/webapp/containers/main/sources/choose-sources/choose-source-modal/index.tsx +++ b/frontend/webapp/containers/main/sources/choose-sources/choose-source-modal/index.tsx @@ -1,16 +1,8 @@ -import styled from 'styled-components'; import React, { useState, useCallback } from 'react'; -import { useActualSources, useConnectSourcesMenuState } from '@/hooks'; import { ChooseSourcesBody } from '../choose-sources-body'; import { Modal, NavigationButtons } from '@/reuseable-components'; import { K8sActualSource, PersistNamespaceItemInput } from '@/types'; - -const ChooseSourcesBodyWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -`; +import { useActualSources, useConnectSourcesMenuState } from '@/hooks'; interface AddSourceModalProps { isOpen: boolean; @@ -65,9 +57,7 @@ export const AddSourceModal: React.FC = ({ isOpen, onClose /> } > - - - + ); }; diff --git a/frontend/webapp/containers/main/sources/choose-sources/choose-sources-body/index.tsx b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-body/index.tsx index 58f401b90..db94d8c6a 100644 --- a/frontend/webapp/containers/main/sources/choose-sources/choose-sources-body/index.tsx +++ b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-body/index.tsx @@ -1,38 +1,22 @@ import React from 'react'; +import { ModalBody } from '@/styles'; import styled from 'styled-components'; import { K8sActualSource } from '@/types'; import { useConnectSourcesList } from '@/hooks'; import { SourcesList } from '../choose-sources-list'; import { SectionTitle, Divider } from '@/reuseable-components'; -import { - SearchAndDropdown, - TogglesAndCheckboxes, -} from '../choose-sources-menu'; -import { - SearchDropdownState, - ToggleCheckboxState, - SearchDropdownHandlers, - ToggleCheckboxHandlers, -} from '../choose-sources-menu/type'; - -const ContentWrapper = styled.div` - width: 100%; - max-width: 640px; - height: 640px; - margin: 0 15vw; - padding-top: 64px; -`; +import { SearchAndDropdown, TogglesAndCheckboxes } from '../choose-sources-menu'; +import { SearchDropdownState, ToggleCheckboxState, SearchDropdownHandlers, ToggleCheckboxHandlers } from '../choose-sources-menu/type'; const SourcesListWrapper = styled.div<{ isModal: boolean }>` display: flex; flex-direction: column; align-items: center; gap: 12px; + max-height: ${({ isModal }) => (isModal ? 'calc(100vh - 548px)' : 'calc(100vh - 360px)')}; height: 100%; padding-bottom: ${({ isModal }) => (isModal ? '48px' : '0')}; - max-height: ${({ isModal }) => - isModal ? 'calc(100vh - 548px)' : 'calc(100vh - 360px)'}; - overflow-y: auto; + overflow-y: scroll; `; interface ChooseSourcesContentProps { @@ -43,13 +27,7 @@ interface ChooseSourcesContentProps { setSourcesList: React.Dispatch>; } -const ChooseSourcesBody: React.FC = ({ - stateMenu, - isModal = false, - sourcesList, - stateHandlers, - setSourcesList, -}) => { +const ChooseSourcesBody: React.FC = ({ stateMenu, isModal = false, sourcesList, stateHandlers, setSourcesList }) => { const { namespacesList } = useConnectSourcesList({ stateMenu, setSourcesList, @@ -57,30 +35,22 @@ const ChooseSourcesBody: React.FC = ({ function getVisibleSources() { const allSources = sourcesList || []; - const filteredSources = stateMenu.searchFilter - ? stateHandlers.filterSources(allSources) - : allSources; + const filteredSources = stateMenu.searchFilter ? stateHandlers.filterSources(allSources) : allSources; return stateMenu.showSelectedOnly ? filteredSources.filter((source) => stateMenu.selectedOption - ? ( - stateMenu.selectedItems[stateMenu.selectedOption.value] || [] - ).find((selectedItem) => selectedItem.name === source.name) + ? (stateMenu.selectedItems[stateMenu.selectedOption.value] || []).find((selectedItem) => selectedItem.name === source.name) : false ) : filteredSources; } const toggleCheckboxState: ToggleCheckboxState = { - selectedAppsCount: stateMenu.selectedOption - ? (stateMenu.selectedItems[stateMenu.selectedOption.value] || []).length - : 0, + selectedAppsCount: stateMenu.selectedOption ? (stateMenu.selectedItems[stateMenu.selectedOption.value] || []).length : 0, selectAllCheckbox: stateMenu.selectAllCheckbox, showSelectedOnly: stateMenu.showSelectedOnly, - futureAppsCheckbox: - stateMenu.futureAppsCheckbox[stateMenu.selectedOption?.value || ''] || - false, + futureAppsCheckbox: stateMenu.futureAppsCheckbox[stateMenu.selectedOption?.value || ''] || false, }; const toggleCheckboxHandlers: ToggleCheckboxHandlers = { @@ -105,34 +75,23 @@ const ChooseSourcesBody: React.FC = ({ }; return ( - + - - - - + + + + - + ); }; diff --git a/frontend/webapp/hooks/actions/useActionCRUD.ts b/frontend/webapp/hooks/actions/useActionCRUD.ts index 1150d43d4..0b95f506f 100644 --- a/frontend/webapp/hooks/actions/useActionCRUD.ts +++ b/frontend/webapp/hooks/actions/useActionCRUD.ts @@ -1,9 +1,10 @@ import { useDrawerStore } from '@/store'; import { useNotify } from '../useNotify'; import { useMutation } from '@apollo/client'; -import type { ActionInput, ActionsType, NotificationType } from '@/types'; import { useComputePlatform } from '../compute-platform'; +import { ACTION, getSseTargetFromId, NOTIFICATION } from '@/utils'; import { CREATE_ACTION, DELETE_ACTION, UPDATE_ACTION } from '@/graphql/mutations'; +import { OVERVIEW_ENTITY_TYPES, type ActionInput, type ActionsType, type NotificationType } from '@/types'; interface UseActionCrudParams { onSuccess?: () => void; @@ -15,41 +16,49 @@ export const useActionCRUD = (params?: UseActionCrudParams) => { const { refetch } = useComputePlatform(); const notify = useNotify(); - const notifyUser = (type: NotificationType, title: string, message: string) => { - notify({ type, title, message }); + const notifyUser = (type: NotificationType, title: string, message: string, id?: string) => { + notify({ + type, + title, + message, + crdType: OVERVIEW_ENTITY_TYPES.ACTION, + target: id ? getSseTargetFromId(id, OVERVIEW_ENTITY_TYPES.ACTION) : undefined, + }); }; - const handleError = (title: string, message: string) => { - notifyUser('error', title, message); + const handleError = (title: string, message: string, id?: string) => { + notifyUser(NOTIFICATION.ERROR, title, message, id); params?.onError?.(); }; - const handleComplete = (title: string, message: string) => { - notifyUser('success', title, message); + const handleComplete = (title: string, message: string, id?: string) => { + notifyUser(NOTIFICATION.SUCCESS, title, message, id); setDrawerItem(null); refetch(); params?.onSuccess?.(); }; const [createAction, cState] = useMutation<{ createAction: { id: string } }>(CREATE_ACTION, { - onError: (error) => handleError('Create', error.message), - onCompleted: (_, req) => { + onError: (error) => handleError(ACTION.CREATE, error.message), + onCompleted: (res, req) => { + const id = res.createAction.id; const name = req?.variables?.action.name || req?.variables?.action.type; - handleComplete('Create', `action "${name}" was created`); + handleComplete(ACTION.CREATE, `action "${name}" was created`, id); }, }); const [updateAction, uState] = useMutation<{ updateAction: { id: string } }>(UPDATE_ACTION, { - onError: (error) => handleError('Update', error.message), - onCompleted: (_, req) => { + onError: (error) => handleError(ACTION.UPDATE, error.message), + onCompleted: (res, req) => { + const id = res.updateAction.id; const name = req?.variables?.action.name || req?.variables?.action.type; - handleComplete('Update', `action "${name}" was updated`); + handleComplete(ACTION.UPDATE, `action "${name}" was updated`, id); }, }); const [deleteAction, dState] = useMutation<{ deleteAction: boolean }>(DELETE_ACTION, { - onError: (error) => handleError('Delete', error.message), - onCompleted: (_, req) => { - const name = req?.variables?.id; - handleComplete('Delete', `action "${name}" was deleted`); + onError: (error) => handleError(ACTION.DELETE, error.message), + onCompleted: (res, req) => { + const id = req?.variables?.id; + handleComplete(ACTION.DELETE, `action "${id}" was deleted`); }, }); diff --git a/frontend/webapp/hooks/actions/useActionFormData.ts b/frontend/webapp/hooks/actions/useActionFormData.ts index 7ddd74e55..9c2b069fa 100644 --- a/frontend/webapp/hooks/actions/useActionFormData.ts +++ b/frontend/webapp/hooks/actions/useActionFormData.ts @@ -1,6 +1,8 @@ import { useState } from 'react'; -import type { ActionDataParsed, ActionInput } from '@/types'; +import { useNotify } from '../useNotify'; import { DrawerBaseItem } from '@/store'; +import { ACTION, FORM_ALERTS, NOTIFICATION } from '@/utils'; +import type { ActionDataParsed, ActionInput } from '@/types'; const INITIAL: ActionInput = { type: '', @@ -12,6 +14,7 @@ const INITIAL: ActionInput = { }; export function useActionFormData() { + const notify = useNotify(); const [formData, setFormData] = useState({ ...INITIAL }); const handleFormChange = (key: keyof typeof INITIAL, val: any) => { @@ -25,7 +28,7 @@ export function useActionFormData() { setFormData({ ...INITIAL }); }; - const validateForm = () => { + const validateForm = (params?: { withAlert?: boolean }) => { let ok = true; Object.entries(formData).forEach(([k, v]) => { @@ -41,6 +44,14 @@ export function useActionFormData() { } }); + if (!ok && params?.withAlert) { + notify({ + type: NOTIFICATION.ERROR, + title: ACTION.UPDATE, + message: FORM_ALERTS.REQUIRED_FIELDS, + }); + } + return ok; }; diff --git a/frontend/webapp/hooks/destinations/useDestinationCRUD.ts b/frontend/webapp/hooks/destinations/useDestinationCRUD.ts index a66d99205..3668cfbe6 100644 --- a/frontend/webapp/hooks/destinations/useDestinationCRUD.ts +++ b/frontend/webapp/hooks/destinations/useDestinationCRUD.ts @@ -2,7 +2,8 @@ import { useDrawerStore } from '@/store'; import { useNotify } from '../useNotify'; import { useMutation } from '@apollo/client'; import { useComputePlatform } from '../compute-platform'; -import type { DestinationInput, NotificationType } from '@/types'; +import { ACTION, getSseTargetFromId, NOTIFICATION } from '@/utils'; +import { OVERVIEW_ENTITY_TYPES, type DestinationInput, type NotificationType } from '@/types'; import { CREATE_DESTINATION, DELETE_DESTINATION, UPDATE_DESTINATION } from '@/graphql/mutations'; interface Params { @@ -15,41 +16,49 @@ export const useDestinationCRUD = (params?: Params) => { const { refetch } = useComputePlatform(); const notify = useNotify(); - const notifyUser = (type: NotificationType, title: string, message: string) => { - notify({ type, title, message }); + const notifyUser = (type: NotificationType, title: string, message: string, id?: string) => { + notify({ + type, + title, + message, + crdType: OVERVIEW_ENTITY_TYPES.DESTINATION, + target: id ? getSseTargetFromId(id, OVERVIEW_ENTITY_TYPES.DESTINATION) : undefined, + }); }; - const handleError = (title: string, message: string) => { - notifyUser('error', title, message); + const handleError = (title: string, message: string, id?: string) => { + notifyUser(NOTIFICATION.ERROR, title, message, id); params?.onError?.(); }; - const handleComplete = (title: string, message: string) => { - notifyUser('success', title, message); + const handleComplete = (title: string, message: string, id?: string) => { + notifyUser(NOTIFICATION.SUCCESS, title, message, id); setDrawerItem(null); refetch(); params?.onSuccess?.(); }; const [createDestination, cState] = useMutation<{ createNewDestination: { id: string } }>(CREATE_DESTINATION, { - onError: (error) => handleError('Create', error.message), - onCompleted: (_, req) => { + onError: (error) => handleError(ACTION.CREATE, error.message), + onCompleted: (res, req) => { + const id = res.createNewDestination.id; const name = req?.variables?.destination.name || req?.variables?.destination.type; - handleComplete('Create', `destination "${name}" was created`); + handleComplete(ACTION.CREATE, `destination "${name}" was created`, id); }, }); const [updateDestination, uState] = useMutation<{ updateDestination: { id: string } }>(UPDATE_DESTINATION, { - onError: (error) => handleError('Update', error.message), - onCompleted: (_, req) => { + onError: (error) => handleError(ACTION.UPDATE, error.message), + onCompleted: (res, req) => { + const id = res.updateDestination.id; const name = req?.variables?.destination.name || req?.variables?.destination.type; - handleComplete('Update', `destination "${name}" was updated`); + handleComplete(ACTION.UPDATE, `destination "${name}" was updated`, id); }, }); const [deleteDestination, dState] = useMutation<{ deleteDestination: boolean }>(DELETE_DESTINATION, { - onError: (error) => handleError('Delete', error.message), - onCompleted: (_, req) => { - const name = req?.variables?.id; - handleComplete('Delete', `destination "${name}" was deleted`); + onError: (error) => handleError(ACTION.DELETE, error.message), + onCompleted: (res, req) => { + const id = req?.variables?.id; + handleComplete(ACTION.DELETE, `destination "${id}" was deleted`); }, }); diff --git a/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleCRUD.ts b/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleCRUD.ts index 819f2006b..debdbccf7 100644 --- a/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleCRUD.ts +++ b/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleCRUD.ts @@ -1,9 +1,9 @@ import { useDrawerStore } from '@/store'; import { useNotify } from '../useNotify'; import { useMutation } from '@apollo/client'; -import { deriveTypeFromRule } from '@/utils'; import { useComputePlatform } from '../compute-platform'; -import type { InstrumentationRuleInput, NotificationType } from '@/types'; +import { ACTION, deriveTypeFromRule, getSseTargetFromId, NOTIFICATION } from '@/utils'; +import { OVERVIEW_ENTITY_TYPES, type InstrumentationRuleInput, type NotificationType } from '@/types'; import { CREATE_INSTRUMENTATION_RULE, UPDATE_INSTRUMENTATION_RULE, DELETE_INSTRUMENTATION_RULE } from '@/graphql/mutations'; interface Params { @@ -16,41 +16,49 @@ export const useInstrumentationRuleCRUD = (params?: Params) => { const { refetch } = useComputePlatform(); const notify = useNotify(); - const notifyUser = (type: NotificationType, title: string, message: string) => { - notify({ type, title, message }); + const notifyUser = (type: NotificationType, title: string, message: string, id?: string) => { + notify({ + type, + title, + message, + crdType: OVERVIEW_ENTITY_TYPES.RULE, + target: id ? getSseTargetFromId(id, OVERVIEW_ENTITY_TYPES.RULE) : undefined, + }); }; - const handleError = (title: string, message: string) => { - notifyUser('error', title, message); + const handleError = (title: string, message: string, id?: string) => { + notifyUser(NOTIFICATION.ERROR, title, message, id); params?.onError?.(); }; - const handleComplete = (title: string, message: string) => { - notifyUser('success', title, message); + const handleComplete = (title: string, message: string, id?: string) => { + notifyUser(NOTIFICATION.SUCCESS, title, message, id); setDrawerItem(null); refetch(); params?.onSuccess?.(); }; const [createInstrumentationRule, cState] = useMutation<{ createInstrumentationRule: { ruleId: string } }>(CREATE_INSTRUMENTATION_RULE, { - onError: (error) => handleError('Create', error.message), - onCompleted: (_, req) => { + onError: (error) => handleError(ACTION.CREATE, error.message), + onCompleted: (res, req) => { + const id = res.createInstrumentationRule.ruleId; const name = req?.variables?.instrumentationRule.ruleName || deriveTypeFromRule(req?.variables?.instrumentationRule); - handleComplete('Create', `instrumentation rule "${name}" was created`); + handleComplete(ACTION.CREATE, `instrumentation rule "${name}" was created`, id); }, }); const [updateInstrumentationRule, uState] = useMutation<{ updateInstrumentationRule: { ruleId: string } }>(UPDATE_INSTRUMENTATION_RULE, { - onError: (error) => handleError('Update', error.message), - onCompleted: (_, req) => { + onError: (error) => handleError(ACTION.UPDATE, error.message), + onCompleted: (res, req) => { + const id = res.updateInstrumentationRule.ruleId; const name = req?.variables?.instrumentationRule.ruleName || deriveTypeFromRule(req?.variables?.instrumentationRule); - handleComplete('Update', `instrumentation rule "${name}" was updated`); + handleComplete(ACTION.UPDATE, `instrumentation rule "${name}" was updated`, id); }, }); const [deleteInstrumentationRule, dState] = useMutation<{ deleteInstrumentationRule: boolean }>(DELETE_INSTRUMENTATION_RULE, { - onError: (error) => handleError('Delete', error.message), - onCompleted: (_, req) => { - const name = req?.variables?.ruleId; - handleComplete('Delete', `instrumentation rule "${name}" was deleted`); + onError: (error) => handleError(ACTION.DELETE, error.message), + onCompleted: (res, req) => { + const id = req?.variables?.ruleId; + handleComplete(ACTION.DELETE, `instrumentation rule "${id}" was deleted`); }, }); diff --git a/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleFormData.ts b/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleFormData.ts index 7dfa31b5c..b99b2b66e 100644 --- a/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleFormData.ts +++ b/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleFormData.ts @@ -1,5 +1,7 @@ import { useState } from 'react'; +import { useNotify } from '../useNotify'; import type { DrawerBaseItem } from '@/store'; +import { ACTION, FORM_ALERTS, NOTIFICATION } from '@/utils'; import { PayloadCollectionType, type InstrumentationRuleInput, type InstrumentationRuleSpec } from '@/types'; const INITIAL: InstrumentationRuleInput = { @@ -17,6 +19,7 @@ const INITIAL: InstrumentationRuleInput = { }; export function useInstrumentationRuleFormData() { + const notify = useNotify(); const [formData, setFormData] = useState({ ...INITIAL }); const handleFormChange = (key: keyof typeof INITIAL, val: any) => { @@ -30,7 +33,7 @@ export function useInstrumentationRuleFormData() { setFormData({ ...INITIAL }); }; - const validateForm = () => { + const validateForm = (params?: { withAlert?: boolean }) => { let ok = true; Object.entries(formData).forEach(([k, v]) => { @@ -45,6 +48,14 @@ export function useInstrumentationRuleFormData() { } }); + if (!ok && params?.withAlert) { + notify({ + type: NOTIFICATION.ERROR, + title: ACTION.UPDATE, + message: FORM_ALERTS.REQUIRED_FIELDS, + }); + } + return ok; }; diff --git a/frontend/webapp/hooks/useSSE.ts b/frontend/webapp/hooks/useSSE.ts index bd52d8daf..26a47a45e 100644 --- a/frontend/webapp/hooks/useSSE.ts +++ b/frontend/webapp/hooks/useSSE.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { API } from '@/utils'; +import { API, NOTIFICATION } from '@/utils'; import { useNotify } from './useNotify'; export function useSSE() { @@ -66,7 +66,7 @@ export function useSSE() { console.error('Max retries reached. Could not reconnect to EventSource.'); notify({ - type: 'error', + type: NOTIFICATION.ERROR, title: 'Connection Error', message: 'Connection to the server failed. Please reboot the application.', }); diff --git a/frontend/webapp/reuseable-components/index.ts b/frontend/webapp/reuseable-components/index.ts index b5b0498d9..b8e7357ee 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -11,7 +11,6 @@ export * from './toggle'; export * from './checkbox'; export * from './modal'; export * from './modal/warning-modal'; -export * from './modal/styled'; export * from './navigation-buttons'; export * from './tag'; export * from './checkbox-list'; @@ -30,4 +29,3 @@ export * from './drawer'; export * from './input-table'; export * from './status'; export * from './field-label'; -export * from './toast-list'; diff --git a/frontend/webapp/reuseable-components/modal/index.tsx b/frontend/webapp/reuseable-components/modal/index.tsx index f27f30b03..8fb0b3a1b 100644 --- a/frontend/webapp/reuseable-components/modal/index.tsx +++ b/frontend/webapp/reuseable-components/modal/index.tsx @@ -4,7 +4,7 @@ import { Text } from '../text'; import ReactDOM from 'react-dom'; import { useKeyDown } from '@/hooks'; import styled from 'styled-components'; -import { slide, Overlay } from '@/styles'; +import { slide, Overlay, CenterThis } from '@/styles'; interface ModalProps { isOpen: boolean; @@ -55,7 +55,7 @@ const HeaderActionsWrapper = styled.div` gap: 8px; `; -const ModalContent = styled.div``; +const ModalContent = styled(CenterThis)``; const ModalTitleContainer = styled.div` position: absolute; @@ -83,7 +83,7 @@ const CancelText = styled(Text)` cursor: pointer; `; -const Modal: React.FC = ({ isOpen, noOverlay, header, onClose, children, actionComponent }) => { +const Modal: React.FC = ({ isOpen, noOverlay, header, actionComponent, onClose, children }) => { useKeyDown( { key: 'Escape', @@ -96,7 +96,7 @@ const Modal: React.FC = ({ isOpen, noOverlay, header, onClose, child return ReactDOM.createPortal( <> - + {header && ( @@ -111,6 +111,7 @@ const Modal: React.FC = ({ isOpen, noOverlay, header, onClose, child {actionComponent} )} + {children} , diff --git a/frontend/webapp/reuseable-components/modal/styled.ts b/frontend/webapp/reuseable-components/modal/styled.ts deleted file mode 100644 index eb962dc3c..000000000 --- a/frontend/webapp/reuseable-components/modal/styled.ts +++ /dev/null @@ -1,21 +0,0 @@ -import styled from 'styled-components'; - -export const ModalContent = styled.section` - width: 100%; - max-width: 640px; - height: 640px; - margin: 0 15vw; - padding: 64px 12px 0 12px; - display: flex; - flex-direction: column; - overflow-y: scroll; -`; - -export const Center = styled.div` - width: 100%; - margin-top: 24px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -`; diff --git a/frontend/webapp/reuseable-components/notification-note/index.tsx b/frontend/webapp/reuseable-components/notification-note/index.tsx index 41ec2c9a9..57f9d91a1 100644 --- a/frontend/webapp/reuseable-components/notification-note/index.tsx +++ b/frontend/webapp/reuseable-components/notification-note/index.tsx @@ -6,9 +6,12 @@ import { Divider } from '../divider'; import styled from 'styled-components'; import { getStatusIcon } from '@/utils'; import { progress, slide } from '@/styles'; -import { useNotificationStore } from '@/store'; import type { Notification, NotificationType } from '@/types'; +interface OnCloseParams { + asSeen: boolean; +} + interface NotificationProps { id?: string; type: NotificationType; @@ -18,6 +21,7 @@ interface NotificationProps { label: string; onClick: () => void; }; + onClose?: (params: OnCloseParams) => void; style?: React.CSSProperties; } @@ -102,9 +106,7 @@ const CloseButton = styled(Image)` } `; -const NotificationNote: React.FC = ({ id, type, title, message, action, style }) => { - const { markAsDismissed, markAsSeen } = useNotificationStore(); - +const NotificationNote: React.FC = ({ type, title, message, action, onClose, style }) => { // These are for handling transitions: // isEntering - to stop the progress bar from rendering before the toast is fully slide-in // isLeaving - to trigger the slide-out animation @@ -116,16 +118,15 @@ const NotificationNote: React.FC = ({ id, type, title, messag const progress = useRef(null); const closeToast = useCallback( - (params?: { asSeen: boolean }) => { - if (!!id) { + (params: OnCloseParams) => { + if (onClose) { setIsLeaving(true); setTimeout(() => { - markAsDismissed(id); - if (params?.asSeen) markAsSeen(id); + onClose({ asSeen: params?.asSeen }); }, TRANSITION_DURATION); } }, - [id] + [onClose] ); useEffect(() => { @@ -137,7 +138,7 @@ const NotificationNote: React.FC = ({ id, type, title, messag }, []); useEffect(() => { - timerForClosure.current = setTimeout(closeToast, TOAST_DURATION); + timerForClosure.current = setTimeout(() => closeToast({ asSeen: false }), TOAST_DURATION); return () => { if (timerForClosure.current) clearTimeout(timerForClosure.current); @@ -153,13 +154,13 @@ const NotificationNote: React.FC = ({ id, type, title, messag if (progress.current) { const remainingTime = (progress.current.offsetWidth / (progress.current.parentElement as HTMLDivElement).offsetWidth) * 4000; - timerForClosure.current = setTimeout(closeToast, remainingTime); + timerForClosure.current = setTimeout(() => closeToast({ asSeen: false }), remainingTime); progress.current.style.animationPlayState = 'running'; } }; return ( - + {type} @@ -169,15 +170,15 @@ const NotificationNote: React.FC = ({ id, type, title, messag {message && {message}} - {(action || id) && ( + {(action || onClose) && ( {action && {action.label}} - {id && closeToast({ asSeen: true })} />} + {onClose && closeToast({ asSeen: true })} />} )} - {!!id && !isEntering && !isLeaving && } + {onClose && !isEntering && !isLeaving && } ); }; diff --git a/frontend/webapp/reuseable-components/toast-list/index.tsx b/frontend/webapp/reuseable-components/toast-list/index.tsx deleted file mode 100644 index f4b9855ed..000000000 --- a/frontend/webapp/reuseable-components/toast-list/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { NotificationNote } from '@/reuseable-components'; -import { Notification, OVERVIEW_ENTITY_TYPES } from '@/types'; -import { DrawerBaseItem, useDrawerStore, useNotificationStore } from '@/store'; -import { useActualDestination, useActualSources, useGetActions, useGetInstrumentationRules } from '@/hooks'; - -const Container = styled.div` - position: fixed; - bottom: 20px; - left: 50%; - transform: translateX(-50%); - z-index: 10000; - display: flex; - flex-direction: column; - gap: 6px; - min-width: 600px; -`; - -export const ToastList: React.FC = () => { - const { notifications } = useNotificationStore(); - - return ( - - {notifications - .filter(({ dismissed }) => !dismissed) - .map((notif) => ( - - ))} - - ); -}; - -const Toast: React.FC = ({ id, type, title, message, crdType, target }) => { - const { markAsDismissed, markAsSeen } = useNotificationStore(); - - const { actions } = useGetActions(); - const { sources } = useActualSources(); - const { destinations } = useActualDestination(); - const { instrumentationRules } = useGetInstrumentationRules(); - const setSelectedItem = useDrawerStore(({ setSelectedItem }) => setSelectedItem); - - const onClick = () => { - const drawerItem: Partial = {}; - - console.log('crdType', crdType); - console.log('target', target); - - switch (crdType) { - case 'Rule': - drawerItem['type'] = OVERVIEW_ENTITY_TYPES.RULE; - // drawerItem['id'] = ''; - // drawerItem['item'] = instrumentationRules.find((item) => item.ruleId === drawerItem['id']); - break; - case 'InstrumentedApplication': - case 'InstrumentationInstance': - drawerItem['type'] = OVERVIEW_ENTITY_TYPES.SOURCE; - // drawerItem['id'] = {}; - // drawerItem['item'] = sources.find((item) => item.kind === drawerItem['id']?.['kind'] && item.name === drawerItem['id']?.['name'] && item.namespace === drawerItem['id']?.['namespace']); - break; - case 'Action': - drawerItem['type'] = OVERVIEW_ENTITY_TYPES.ACTION; - // drawerItem['id'] = ''; - // drawerItem['item'] = actions.find((item) => item.id === drawerItem['id']); - break; - case 'Destination': - drawerItem['type'] = OVERVIEW_ENTITY_TYPES.DESTINATION; - // drawerItem['id'] = ''; - // drawerItem['item'] = destinations.find((item) => item.id === drawerItem['id']); - break; - - default: - break; - } - - console.log('drawerItem', drawerItem); - - if (!!drawerItem.item) { - setSelectedItem(drawerItem as DrawerBaseItem); - markAsSeen(id); - markAsDismissed(id); - } - }; - - return ( - - ); -}; diff --git a/frontend/webapp/styles/styled.tsx b/frontend/webapp/styles/styled.tsx index 8488ee805..22963b028 100644 --- a/frontend/webapp/styles/styled.tsx +++ b/frontend/webapp/styles/styled.tsx @@ -9,7 +9,15 @@ export const HideScroll = styled.div` scrollbar-width: none; `; -export const Overlay = styled.div<{ hideOverlay?: boolean }>` +export const CenterThis = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +export const Overlay = styled.div` position: fixed; top: 0; left: 0; @@ -18,5 +26,14 @@ export const Overlay = styled.div<{ hideOverlay?: boolean }>` z-index: 1000; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(1px); - opacity: ${({ hideOverlay }) => (hideOverlay ? 0 : 1)}; +`; + +// this is to control modal size + scroll +// note: add-destinations does not use this (yet), because it has a custom sidebar +export const ModalBody = styled.div` + min-width: 640px; + height: calc(100vh - 300px); + margin: 0 15vw; + padding-top: 64px; + overflow-y: scroll; `; diff --git a/frontend/webapp/utils/constants/string.tsx b/frontend/webapp/utils/constants/string.tsx index 87481b578..de673f550 100644 --- a/frontend/webapp/utils/constants/string.tsx +++ b/frontend/webapp/utils/constants/string.tsx @@ -1,3 +1,5 @@ +import type { NotificationType } from '@/types'; + export const SETUP = { STEPS: { CHOOSE_SOURCE: 'Choose Source', @@ -94,11 +96,9 @@ export const OVERVIEW = { ACTION_DANGER_ZONE_TITLE: 'Delete this action', SOURCE_DANGER_ZONE_SUBTITLE: 'Uninstrument this source, and delete all odigos associated data. You can always re-instrument this source later with odigos.', - ACTION_DANGER_ZONE_SUBTITLE: - 'This action cannot be undone. This will permanently delete the action and all associated data.', + ACTION_DANGER_ZONE_SUBTITLE: 'This action cannot be undone. This will permanently delete the action and all associated data.', DELETE_MODAL_TITLE: 'Delete this destination', - DELETE_MODAL_SUBTITLE: - 'This action cannot be undone. This will permanently delete the destination and all associated data.', + DELETE_MODAL_SUBTITLE: 'This action cannot be undone. This will permanently delete the destination and all associated data.', DELETE_BUTTON: 'I want to delete this destination', CONFIRM_SOURCE_DELETE: 'I want to delete this source', CONFIRM_DELETE_ACTION: 'I want to delete this action', @@ -110,8 +110,7 @@ export const OVERVIEW = { 'Actions are a way to modify the OpenTelemetry data recorded by Odigos Sources, before it is exported to your Odigos Destinations.', CREATE_INSTRUMENTATION_RULE: 'Create Instrumentation Rule', EDIT_INSTRUMENTATION_RULE: 'Edit Instrumentation Rule', - INSTRUMENTATION_RULE_DESCRIPTION: - 'Instrumentation Rules control how telemetry is recorded from your application.', + INSTRUMENTATION_RULE_DESCRIPTION: 'Instrumentation Rules control how telemetry is recorded from your application.', }; export const ACTION = { @@ -119,14 +118,23 @@ export const ACTION = { CONTACT_US: 'Contact Us', LEARN_MORE: 'Learn more', LINK_TO_DOCS: 'Link to docs', + ENABLE: 'Enable', DISABLE: 'Disable', RUNNING: 'Running', APPLIED: 'Applied', - ENABLE: 'Enable', DELETE_ALL: 'Delete All', + CREATE: 'Create', + UPDATE: 'Update', + DELETE: 'Delete', +}; + +export const FORM_ALERTS = { + REQUIRED_FIELDS: 'Required fields are missing!', }; -export const NOTIFICATION = { +export const NOTIFICATION: { + [key: string]: NotificationType; +} = { ERROR: 'error', SUCCESS: 'success', }; diff --git a/frontend/webapp/utils/functions/extract-monitors.ts b/frontend/webapp/utils/functions/extract-monitors.ts deleted file mode 100644 index e552ecfc3..000000000 --- a/frontend/webapp/utils/functions/extract-monitors.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ExportedSignals } from '@/types'; - -export const extractMonitors = (exportedSignals: ExportedSignals) => { - const filtered = Object.keys(exportedSignals).filter((signal) => exportedSignals[signal] === true); - - return filtered; -}; diff --git a/frontend/webapp/utils/functions/formatters.ts b/frontend/webapp/utils/functions/formatters.ts index 11e0f4452..ed7c86f81 100644 --- a/frontend/webapp/utils/functions/formatters.ts +++ b/frontend/webapp/utils/functions/formatters.ts @@ -1,3 +1,5 @@ +import { ExportedSignals, OVERVIEW_ENTITY_TYPES, WorkloadId } from '@/types'; + export const formatBytes = (bytes?: number) => { if (!bytes) return '0 KB/s'; @@ -7,3 +9,50 @@ export const formatBytes = (bytes?: number) => { return `${value.toFixed(1)} ${sizes[i]}`; }; + +export const extractMonitors = (exportedSignals: ExportedSignals) => { + const filtered = Object.keys(exportedSignals).filter((signal) => exportedSignals[signal] === true); + + return filtered; +}; + +export const getIdFromSseTarget = (target: string, type: OVERVIEW_ENTITY_TYPES) => { + switch (type) { + case OVERVIEW_ENTITY_TYPES.SOURCE: { + const id: WorkloadId = { + namespace: '', + name: '', + kind: '', + }; + + target.split('&').forEach((str) => { + const [key, value] = str.split('='); + id[key] = value; + }); + + return id; + } + + default: + return target as string; + } +}; + +export const getSseTargetFromId = (id: string | WorkloadId, type: OVERVIEW_ENTITY_TYPES) => { + switch (type) { + case OVERVIEW_ENTITY_TYPES.SOURCE: { + let target = ''; + + Object.entries(id as WorkloadId).forEach(([key, value]) => { + target += `${key}=${value}&`; + }); + + target.slice(0, -1); + + return target; + } + + default: + return id as string; + } +}; diff --git a/frontend/webapp/utils/functions/index.ts b/frontend/webapp/utils/functions/index.ts index dd67708ea..b8aac660b 100644 --- a/frontend/webapp/utils/functions/index.ts +++ b/frontend/webapp/utils/functions/index.ts @@ -1,5 +1,4 @@ export * from './derive-types'; -export * from './extract-monitors'; export * from './formatters'; export * from './get-value-for-range'; export * from './icons';