From 851beb45fc9ed1c045964744682e610629c8b953 Mon Sep 17 00:00:00 2001 From: Ben Elferink <ben.elferink@icloud.com> Date: Sun, 15 Dec 2024 12:51:43 +0200 Subject: [PATCH] [GEN-1730]: add "describe odigos" to UI (#1988) This pull request introduces a new feature to describe Odigos and includes various changes to integrate this feature into the application. The most important changes include the addition of a new `DescribeOdigos` component, updates to existing components to incorporate this new feature, and the creation of a GraphQL query and hook for fetching Odigos data. ### New Feature: Describe Odigos * [`frontend/webapp/components/describe-odigos/index.tsx`](diffhunk://#diff-2e0eca8180f4d2737c72f676dc6caa03ab9295ad5a2d82a90d399e344e0b580eR1-R16): Added the `DescribeOdigos` component which includes an icon button to trigger the display of Odigos information. * [`frontend/webapp/graphql/queries/describe.ts`](diffhunk://#diff-1dd956050209ccf1d679de5ca93cc29b9ccddc56c27947dbf5f9255ea4b8d0c5R3-R143): Created a new GraphQL query `DESCRIBE_ODIGOS` to fetch Odigos data. * [`frontend/webapp/hooks/describe/useDescribeOdigos.ts`](diffhunk://#diff-f96e0d4e8e6b30c653c21364c7479cd8656417588a42c68aa9346c90e838d4ccR1-R15): Added a custom hook `useDescribeOdigos` to use the `DESCRIBE_ODIGOS` query. ### Component Integration * [`frontend/webapp/components/main/header/index.tsx`](diffhunk://#diff-2c96f91ec30d2116981a9c0a562820ff9fd87c8292cb5dca11a45d6fb2ac6c04L9-R9): Integrated the `DescribeOdigos` component into the main header. [[1]](diffhunk://#diff-2c96f91ec30d2116981a9c0a562820ff9fd87c8292cb5dca11a45d6fb2ac6c04L9-R9) [[2]](diffhunk://#diff-2c96f91ec30d2116981a9c0a562820ff9fd87c8292cb5dca11a45d6fb2ac6c04R45) * [`frontend/webapp/components/overview/all-drawers/index.tsx`](diffhunk://#diff-bf25245ffa5cb1c7ea54b941a97fc9f53caf28b7154cb4eda9b88c7b6f0944d1L2-R10): Updated the `AllDrawers` component to include a case for `DRAWER_OTHER_TYPES.DESCRIBE_ODIGOS`, which renders the `DescribeDrawer` component. [[1]](diffhunk://#diff-bf25245ffa5cb1c7ea54b941a97fc9f53caf28b7154cb4eda9b88c7b6f0944d1L2-R10) [[2]](diffhunk://#diff-bf25245ffa5cb1c7ea54b941a97fc9f53caf28b7154cb4eda9b88c7b6f0944d1R25-R27) ### Reusable Component Enhancements * [`frontend/webapp/reuseable-components/icon-button/index.tsx`](diffhunk://#diff-d02358b5454137fd2842c4765b1b55d50282fea6338be0617b67385956097896R1-R63): Created a new `IconButton` component with optional ping animation to be used in various parts of the application. ### Code Refactoring and Cleanup * [`frontend/webapp/components/index.ts`](diffhunk://#diff-9a255ecda06fb13562b464a919eb789a51ea8728251b2aa31c28babfd8f3405dL1-R7): Reordered exports for better organization and added export for the new `describe-odigos` component. * [`frontend/webapp/components/notification/notification-manager.tsx`](diffhunk://#diff-57b5151297ef94f210fd8223cf1e64f73a19c43cc5f57f94cc74338251942c35L9-R10): Replaced the custom `BellIcon` with the new `IconButton` component for consistency. [[1]](diffhunk://#diff-57b5151297ef94f210fd8223cf1e64f73a19c43cc5f57f94cc74338251942c35L9-R10) [[2]](diffhunk://#diff-57b5151297ef94f210fd8223cf1e64f73a19c43cc5f57f94cc74338251942c35L109-R87) * [`frontend/webapp/containers/main/overview/overview-drawer/index.tsx`](diffhunk://#diff-2410bbb07cf40a69a4bc6a34db093cdfcc2cb9d4b98b144a37a2c6a3e1a511ffL69-R71): Made several properties optional and added conditional checks to handle undefined properties gracefully. [[1]](diffhunk://#diff-2410bbb07cf40a69a4bc6a34db093cdfcc2cb9d4b98b144a37a2c6a3e1a511ffL69-R71) [[2]](diffhunk://#diff-2410bbb07cf40a69a4bc6a34db093cdfcc2cb9d4b98b144a37a2c6a3e1a511ffR90-R95) [[3]](diffhunk://#diff-732721edf7c32d883ffefa52d6e43339c6ee328c88c3cb5f0d87f6760347b599L17-R22) [[4]](diffhunk://#diff-732721edf7c32d883ffefa52d6e43339c6ee328c88c3cb5f0d87f6760347b599L37-R37) [[5]](diffhunk://#diff-732721edf7c32d883ffefa52d6e43339c6ee328c88c3cb5f0d87f6760347b599L55-R55) [[6]](diffhunk://#diff-732721edf7c32d883ffefa52d6e43339c6ee328c88c3cb5f0d87f6760347b599L67-R67) [[7]](diffhunk://#diff-732721edf7c32d883ffefa52d6e43339c6ee328c88c3cb5f0d87f6760347b599L81-R81) [[8]](diffhunk://#diff-732721edf7c32d883ffefa52d6e43339c6ee328c88c3cb5f0d87f6760347b599L90-R90) [[9]](diffhunk://#diff-732721edf7c32d883ffefa52d6e43339c6ee328c88c3cb5f0d87f6760347b599L106-R114) [[10]](diffhunk://#diff-732721edf7c32d883ffefa52d6e43339c6ee328c88c3cb5f0d87f6760347b599L116-R124) --- .../components/describe-odigos/index.tsx | 16 ++ frontend/webapp/components/index.ts | 5 +- .../webapp/components/main/header/index.tsx | 3 +- .../notification/notification-manager.tsx | 31 +--- .../overview/all-drawers/describe-drawer.tsx | 34 +++++ .../components/overview/all-drawers/index.tsx | 8 +- .../overview-drawer/drawer-header/index.tsx | 18 ++- .../main/overview/overview-drawer/index.tsx | 34 +++-- frontend/webapp/graphql/queries/describe.ts | 141 ++++++++++++++++++ .../webapp/hooks/actions/useActionFormData.ts | 4 +- frontend/webapp/hooks/describe/index.ts | 1 + .../hooks/describe/useDescribeOdigos.ts | 15 ++ .../destinations/useDestinationFormData.ts | 4 +- .../useInstrumentationRuleFormData.ts | 4 +- .../hooks/notification/useClickNotif.ts | 6 +- frontend/webapp/hooks/overview/useMetrics.ts | 14 +- .../reuseable-components/data-card/index.tsx | 21 ++- .../icon-button/index.tsx | 63 ++++++++ frontend/webapp/reuseable-components/index.ts | 1 + frontend/webapp/store/useDrawerStore.ts | 14 +- frontend/webapp/types/describe.ts | 40 +++++ frontend/webapp/utils/constants/string.tsx | 3 +- 22 files changed, 393 insertions(+), 87 deletions(-) create mode 100644 frontend/webapp/components/describe-odigos/index.tsx create mode 100644 frontend/webapp/components/overview/all-drawers/describe-drawer.tsx create mode 100644 frontend/webapp/hooks/describe/useDescribeOdigos.ts create mode 100644 frontend/webapp/reuseable-components/icon-button/index.tsx diff --git a/frontend/webapp/components/describe-odigos/index.tsx b/frontend/webapp/components/describe-odigos/index.tsx new file mode 100644 index 000000000..28c78de36 --- /dev/null +++ b/frontend/webapp/components/describe-odigos/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import Image from 'next/image'; +import theme from '@/styles/theme'; +import { IconButton } from '@/reuseable-components'; +import { DRAWER_OTHER_TYPES, useDrawerStore } from '@/store'; + +export const DescribeOdigos = () => { + const { setSelectedItem } = useDrawerStore(); + const handleClick = () => setSelectedItem({ type: DRAWER_OTHER_TYPES.DESCRIBE_ODIGOS, id: DRAWER_OTHER_TYPES.DESCRIBE_ODIGOS }); + + return ( + <IconButton onClick={handleClick} withPing pingColor={theme.colors.majestic_blue}> + <Image src='/brand/odigos-icon.svg' alt='logo' width={16} height={16} /> + </IconButton> + ); +}; diff --git a/frontend/webapp/components/index.ts b/frontend/webapp/components/index.ts index 73363b759..c23fdeebd 100644 --- a/frontend/webapp/components/index.ts +++ b/frontend/webapp/components/index.ts @@ -1,6 +1,7 @@ -export * from './setup'; -export * from './overview'; export * from './common'; +export * from './describe-odigos'; export * from './main'; export * from './modals'; export * from './notification'; +export * from './overview'; +export * from './setup'; diff --git a/frontend/webapp/components/main/header/index.tsx b/frontend/webapp/components/main/header/index.tsx index fbfb1be26..188732d39 100644 --- a/frontend/webapp/components/main/header/index.tsx +++ b/frontend/webapp/components/main/header/index.tsx @@ -6,7 +6,7 @@ import { PlatformTypes } from '@/types'; import { PlatformTitle } from './cp-title'; import { useConnectionStore } from '@/store'; import { ConnectionStatus } from '@/reuseable-components'; -import { NotificationManager } from '@/components/notification'; +import { DescribeOdigos, NotificationManager } from '@/components'; interface MainHeaderProps {} @@ -42,6 +42,7 @@ export const MainHeader: React.FC<MainHeaderProps> = () => { <AlignRight> <NotificationManager /> + <DescribeOdigos /> </AlignRight> </HeaderContainer> ); diff --git a/frontend/webapp/components/notification/notification-manager.tsx b/frontend/webapp/components/notification/notification-manager.tsx index 20cb5c66f..36586c700 100644 --- a/frontend/webapp/components/notification/notification-manager.tsx +++ b/frontend/webapp/components/notification/notification-manager.tsx @@ -6,32 +6,8 @@ import { useNotificationStore } from '@/store'; import { ACTION, getStatusIcon } from '@/utils'; import { useOnClickOutside, useTimeAgo } from '@/hooks'; import theme, { hexPercentValues } from '@/styles/theme'; -import { NoDataFound, Text } from '@/reuseable-components'; import type { Notification, NotificationType } from '@/types'; - -const BellIcon = styled.div` - position: relative; - width: 36px; - height: 36px; - border-radius: 100%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - &:hover { - background-color: ${({ theme }) => theme.colors.white_opacity['008']}; - } -`; - -const LiveBadge = styled.div` - position: absolute; - top: 8px; - right: 8px; - width: 6px; - height: 6px; - border-radius: 100%; - background-color: ${({ theme }) => theme.colors.orange_og}; -`; +import { IconButton, NoDataFound, Text } from '@/reuseable-components'; const RelativeContainer = styled.div` position: relative; @@ -106,10 +82,9 @@ export const NotificationManager = () => { return ( <RelativeContainer ref={containerRef}> - <BellIcon onClick={toggleOpen}> - {!!unseenCount && <LiveBadge />} + <IconButton onClick={toggleOpen} withPing={!!unseenCount} pingColor={theme.colors.orange_og}> <Image src='/icons/common/notification.svg' alt='logo' width={16} height={16} /> - </BellIcon> + </IconButton> {isOpen && ( <AbsoluteContainer> diff --git a/frontend/webapp/components/overview/all-drawers/describe-drawer.tsx b/frontend/webapp/components/overview/all-drawers/describe-drawer.tsx new file mode 100644 index 000000000..4b893587f --- /dev/null +++ b/frontend/webapp/components/overview/all-drawers/describe-drawer.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import styled from 'styled-components'; +import { useDescribeOdigos } from '@/hooks'; +import { DATA_CARDS, safeJsonStringify } from '@/utils'; +import { DataCard, DataCardFieldTypes } from '@/reuseable-components'; +import OverviewDrawer from '@/containers/main/overview/overview-drawer'; + +interface Props {} + +const DataContainer = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const DescribeDrawer: React.FC<Props> = () => { + const { data: describe } = useDescribeOdigos(); + + return ( + <OverviewDrawer title={DATA_CARDS.DESCRIBE_ODIGOS} titleTooltip='' imageUri='/brand/odigos-icon.svg'> + <DataContainer> + <DataCard + title='' + data={[ + { + type: DataCardFieldTypes.CODE, + value: JSON.stringify({ language: 'json', code: safeJsonStringify(describe) }), + }, + ]} + /> + </DataContainer> + </OverviewDrawer> + ); +}; diff --git a/frontend/webapp/components/overview/all-drawers/index.tsx b/frontend/webapp/components/overview/all-drawers/index.tsx index 65cbb3b59..8f9329437 100644 --- a/frontend/webapp/components/overview/all-drawers/index.tsx +++ b/frontend/webapp/components/overview/all-drawers/index.tsx @@ -1,12 +1,13 @@ import React from 'react'; -import { useDrawerStore } from '@/store'; import { OVERVIEW_ENTITY_TYPES } from '@/types'; +import { DescribeDrawer } from './describe-drawer'; +import { DRAWER_OTHER_TYPES, useDrawerStore } from '@/store'; import { ActionDrawer, DestinationDrawer, RuleDrawer, SourceDrawer } from '@/containers'; const AllDrawers = () => { const selected = useDrawerStore(({ selectedItem }) => selectedItem); - if (!selected?.item) return null; + if (!selected?.type) return null; switch (selected.type) { case OVERVIEW_ENTITY_TYPES.RULE: @@ -21,6 +22,9 @@ const AllDrawers = () => { case OVERVIEW_ENTITY_TYPES.DESTINATION: return <DestinationDrawer />; + case DRAWER_OTHER_TYPES.DESCRIBE_ODIGOS: + return <DescribeDrawer />; + default: return <></>; } diff --git a/frontend/webapp/containers/main/overview/overview-drawer/drawer-header/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/drawer-header/index.tsx index ea94656d2..c7a78f08a 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/drawer-header/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/drawer-header/index.tsx @@ -66,9 +66,9 @@ export interface DrawerHeaderRef { interface DrawerHeaderProps { title: string; titleTooltip?: string; - imageUri: string; - isEdit: boolean; - onEdit: () => void; + imageUri?: string; + isEdit?: boolean; + onEdit?: () => void; onClose: () => void; } @@ -87,9 +87,12 @@ const DrawerHeader = forwardRef<DrawerHeaderRef, DrawerHeaderProps>(({ title, ti return ( <HeaderContainer> <SectionItemsWrapper> - <DrawerItemImageWrapper> - <Image src={imageUri} alt='Drawer Item' width={16} height={16} /> - </DrawerItemImageWrapper> + {!!imageUri && ( + <DrawerItemImageWrapper> + <Image src={imageUri} alt='Drawer Item' width={16} height={16} /> + </DrawerItemImageWrapper> + )} + {!isEdit && ( <Tooltip text={titleTooltip} withIcon> <Title>{title}</Title> @@ -105,12 +108,13 @@ const DrawerHeader = forwardRef<DrawerHeaderRef, DrawerHeaderProps>(({ title, ti )} <SectionItemsWrapper $gap={8}> - {!isEdit && ( + {!isEdit && !!onEdit && ( <EditButton data-id='drawer-edit' variant='tertiary' onClick={onEdit}> <Image src='/icons/common/edit.svg' alt='Edit' width={16} height={16} /> <ButtonText>Edit</ButtonText> </EditButton> )} + <CloseButton data-id='drawer-close' variant='secondary' onClick={onClose}> <Image src='/icons/common/x.svg' alt='Close' width={12} height={12} /> </CloseButton> diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index bf722f7ad..650187ac4 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -14,12 +14,12 @@ interface Props { title: string; titleTooltip?: string; imageUri: string; - isEdit: boolean; - isFormDirty: boolean; - onEdit: (bool?: boolean) => void; - onSave: (newTitle: string) => void; - onDelete: () => void; - onCancel: () => void; + isEdit?: boolean; + isFormDirty?: boolean; + onEdit?: (bool?: boolean) => void; + onSave?: (newTitle: string) => void; + onDelete?: () => void; + onCancel?: () => void; } const DrawerContent = styled.div` @@ -34,7 +34,7 @@ const ContentArea = styled.div` overflow-y: auto; `; -const OverviewDrawer: React.FC<Props & PropsWithChildren> = ({ children, title, titleTooltip, imageUri, isEdit, isFormDirty, onEdit, onSave, onDelete, onCancel }) => { +const OverviewDrawer: React.FC<Props & PropsWithChildren> = ({ children, title, titleTooltip, imageUri, isEdit = false, isFormDirty = false, onEdit, onSave, onDelete, onCancel }) => { const { selectedItem, setSelectedItem } = useDrawerStore(); useKeyDown({ key: 'Enter', active: !!selectedItem }, () => (isEdit ? clickSave() : closeDrawer())); @@ -52,7 +52,7 @@ const OverviewDrawer: React.FC<Props & PropsWithChildren> = ({ children, title, const closeDrawer = () => { setSelectedItem(null); - onEdit(false); + if (onEdit) onEdit(false); setIsDeleteModalOpen(false); setIsCancelModalOpen(false); }; @@ -64,7 +64,7 @@ const OverviewDrawer: React.FC<Props & PropsWithChildren> = ({ children, title, const handleCancel = () => { titleRef.current?.clearTitle(); - onCancel(); + if (onCancel) onCancel(); closeWarningModals(); }; @@ -78,7 +78,7 @@ const OverviewDrawer: React.FC<Props & PropsWithChildren> = ({ children, title, }; const handleDelete = () => { - onDelete(); + if (onDelete) onDelete(); closeWarningModals(); }; @@ -87,7 +87,7 @@ const OverviewDrawer: React.FC<Props & PropsWithChildren> = ({ children, title, }; const clickSave = () => { - onSave(titleRef.current?.getTitle() || ''); + if (onSave) onSave(titleRef.current?.getTitle() || ''); }; const isLastItem = () => { @@ -103,7 +103,15 @@ const OverviewDrawer: React.FC<Props & PropsWithChildren> = ({ children, title, <> <Drawer isOpen onClose={isEdit ? clickCancel : closeDrawer} width={DRAWER_WIDTH} closeOnEscape={!isDeleteModalOpen && !isCancelModalOpen}> <DrawerContent> - <DrawerHeader ref={titleRef} title={title} titleTooltip={titleTooltip} imageUri={imageUri} isEdit={isEdit} onEdit={() => onEdit(true)} onClose={isEdit ? clickCancel : closeDrawer} /> + <DrawerHeader + ref={titleRef} + title={title} + titleTooltip={titleTooltip} + imageUri={imageUri} + isEdit={isEdit} + onEdit={onEdit ? () => onEdit(true) : undefined} + onClose={isEdit ? clickCancel : closeDrawer} + /> <ContentArea>{children}</ContentArea> <DrawerFooter isOpen={isEdit} onSave={clickSave} onCancel={clickCancel} onDelete={clickDelete} deleteLabel={isSource ? 'Uninstrument' : undefined} /> </DrawerContent> @@ -113,7 +121,7 @@ const OverviewDrawer: React.FC<Props & PropsWithChildren> = ({ children, title, isOpen={isDeleteModalOpen} noOverlay name={`${selectedItem?.type}${title ? ` (${title})` : ''}`} - type={selectedItem?.type} + type={selectedItem?.type as OVERVIEW_ENTITY_TYPES} isLastItem={isLastItem()} onApprove={handleDelete} onDeny={closeWarningModals} diff --git a/frontend/webapp/graphql/queries/describe.ts b/frontend/webapp/graphql/queries/describe.ts index b2207ef9c..1280a312e 100644 --- a/frontend/webapp/graphql/queries/describe.ts +++ b/frontend/webapp/graphql/queries/describe.ts @@ -1,5 +1,146 @@ import { gql } from '@apollo/client'; +export const DESCRIBE_ODIGOS = gql` + query DescribeOdigos { + describeOdigos { + odigosVersion { + name + value + status + explain + } + numberOfDestinations + numberOfSources + clusterCollector { + enabled { + name + value + status + explain + } + collectorGroup { + name + value + status + explain + } + deployed { + name + value + status + explain + } + deployedError { + name + value + status + explain + } + collectorReady { + name + value + status + explain + } + deploymentCreated { + name + value + status + explain + } + expectedReplicas { + name + value + status + explain + } + healthyReplicas { + name + value + status + explain + } + failedReplicas { + name + value + status + explain + } + failedReplicasReason { + name + value + status + explain + } + } + nodeCollector { + enabled { + name + value + status + explain + } + collectorGroup { + name + value + status + explain + } + deployed { + name + value + status + explain + } + deployedError { + name + value + status + explain + } + collectorReady { + name + value + status + explain + } + daemonSet { + name + value + status + explain + } + desiredNodes { + name + value + status + explain + } + currentNodes { + name + value + status + explain + } + updatedNodes { + name + value + status + explain + } + availableNodes { + name + value + status + explain + } + } + isSettled + hasErrors + } + } +`; + export const DESCRIBE_SOURCE = gql` query DescribeSource($namespace: String!, $kind: String!, $name: String!) { describeSource(namespace: $namespace, kind: $kind, name: $name) { diff --git a/frontend/webapp/hooks/actions/useActionFormData.ts b/frontend/webapp/hooks/actions/useActionFormData.ts index e6f721462..03d0ee864 100644 --- a/frontend/webapp/hooks/actions/useActionFormData.ts +++ b/frontend/webapp/hooks/actions/useActionFormData.ts @@ -1,4 +1,4 @@ -import { DrawerBaseItem } from '@/store'; +import { DrawerItem } from '@/store'; import { useGenericForm, useNotify } from '@/hooks'; import { FORM_ALERTS, NOTIFICATION } from '@/utils'; import type { ActionDataParsed, ActionInput } from '@/types'; @@ -50,7 +50,7 @@ export function useActionFormData() { return ok; }; - const loadFormWithDrawerItem = (drawerItem: DrawerBaseItem) => { + const loadFormWithDrawerItem = (drawerItem: DrawerItem) => { const { type, spec } = drawerItem.item as ActionDataParsed; const updatedData: ActionInput = { diff --git a/frontend/webapp/hooks/describe/index.ts b/frontend/webapp/hooks/describe/index.ts index 0d183ae60..16e5b9b9d 100644 --- a/frontend/webapp/hooks/describe/index.ts +++ b/frontend/webapp/hooks/describe/index.ts @@ -1 +1,2 @@ +export * from './useDescribeOdigos'; export * from './useDescribeSource'; diff --git a/frontend/webapp/hooks/describe/useDescribeOdigos.ts b/frontend/webapp/hooks/describe/useDescribeOdigos.ts new file mode 100644 index 000000000..74b90199e --- /dev/null +++ b/frontend/webapp/hooks/describe/useDescribeOdigos.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@apollo/client'; +import { DESCRIBE_ODIGOS } from '@/graphql'; +import type { DescribeOdigos } from '@/types'; + +export const useDescribeOdigos = () => { + const { data, loading, error } = useQuery<DescribeOdigos>(DESCRIBE_ODIGOS, { + pollInterval: 5000, + }); + + return { + data: data?.describeOdigos, + loading, + error, + }; +}; diff --git a/frontend/webapp/hooks/destinations/useDestinationFormData.ts b/frontend/webapp/hooks/destinations/useDestinationFormData.ts index 3f4edefd3..07927d7e0 100644 --- a/frontend/webapp/hooks/destinations/useDestinationFormData.ts +++ b/frontend/webapp/hooks/destinations/useDestinationFormData.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { DrawerBaseItem } from '@/store'; +import { DrawerItem } from '@/store'; import { useQuery } from '@apollo/client'; import { GET_DESTINATION_TYPE_DETAILS } from '@/graphql'; import { useConnectDestinationForm, useGenericForm, useNotify } from '@/hooks'; @@ -110,7 +110,7 @@ export function useDestinationFormData(params?: { destinationType?: string; supp return ok; }; - const loadFormWithDrawerItem = (drawerItem: DrawerBaseItem) => { + const loadFormWithDrawerItem = (drawerItem: DrawerItem) => { const { destinationType: { type }, name, diff --git a/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleFormData.ts b/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleFormData.ts index c1718e65f..6fe9b7232 100644 --- a/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleFormData.ts +++ b/frontend/webapp/hooks/instrumentation-rules/useInstrumentationRuleFormData.ts @@ -1,4 +1,4 @@ -import type { DrawerBaseItem } from '@/store'; +import type { DrawerItem } from '@/store'; import { useGenericForm, useNotify } from '@/hooks'; import { FORM_ALERTS, NOTIFICATION } from '@/utils'; import { PayloadCollectionType, type InstrumentationRuleInput, type InstrumentationRuleSpec } from '@/types'; @@ -53,7 +53,7 @@ export function useInstrumentationRuleFormData() { return ok; }; - const loadFormWithDrawerItem = (drawerItem: DrawerBaseItem) => { + const loadFormWithDrawerItem = (drawerItem: DrawerItem) => { const { ruleName, notes, disabled, payloadCollection } = drawerItem.item as InstrumentationRuleSpec; const updatedData: InstrumentationRuleInput = { diff --git a/frontend/webapp/hooks/notification/useClickNotif.ts b/frontend/webapp/hooks/notification/useClickNotif.ts index 02d05a450..a9ba5a970 100644 --- a/frontend/webapp/hooks/notification/useClickNotif.ts +++ b/frontend/webapp/hooks/notification/useClickNotif.ts @@ -4,7 +4,7 @@ import { getIdFromSseTarget } from '@/utils'; import { useDestinationCRUD } from '../destinations'; import { type Notification, OVERVIEW_ENTITY_TYPES } from '@/types'; import { useInstrumentationRuleCRUD } from '../instrumentation-rules'; -import { DrawerBaseItem, useDrawerStore, useNotificationStore } from '@/store'; +import { DrawerItem, useDrawerStore, useNotificationStore } from '@/store'; export const useClickNotif = () => { const { sources } = useSourceCRUD(); @@ -19,7 +19,7 @@ export const useClickNotif = () => { const { dismissToast } = options || {}; if (crdType && target) { - const drawerItem: Partial<DrawerBaseItem> = {}; + const drawerItem: Partial<DrawerItem> = {}; switch (crdType) { case OVERVIEW_ENTITY_TYPES.RULE: @@ -55,7 +55,7 @@ export const useClickNotif = () => { } if (!!drawerItem.item) { - setSelectedItem(drawerItem as DrawerBaseItem); + setSelectedItem(drawerItem as DrawerItem); } else { console.warn('notif item not found for:', { crdType, target }); } diff --git a/frontend/webapp/hooks/overview/useMetrics.ts b/frontend/webapp/hooks/overview/useMetrics.ts index 5fcc8f868..2f6be9934 100644 --- a/frontend/webapp/hooks/overview/useMetrics.ts +++ b/frontend/webapp/hooks/overview/useMetrics.ts @@ -1,15 +1,11 @@ -import { useEffect } from 'react'; import { useQuery } from '@apollo/client'; import { GET_METRICS } from '@/graphql/mutations/metrics'; import type { OverviewMetricsResponse } from '@/types'; -export function useMetrics() { - const { data, refetch } = useQuery<OverviewMetricsResponse>(GET_METRICS); - - useEffect(() => { - const interval = setInterval(async () => await refetch(), 3000); - return () => clearInterval(interval); - }, [refetch]); +export const useMetrics = () => { + const { data } = useQuery<OverviewMetricsResponse>(GET_METRICS, { + pollInterval: 3000, + }); return { metrics: data }; -} +}; diff --git a/frontend/webapp/reuseable-components/data-card/index.tsx b/frontend/webapp/reuseable-components/data-card/index.tsx index 7105834e4..6c7788944 100644 --- a/frontend/webapp/reuseable-components/data-card/index.tsx +++ b/frontend/webapp/reuseable-components/data-card/index.tsx @@ -43,14 +43,19 @@ const Description = styled(Text)` export const DataCard: React.FC<Props> = ({ title = 'Details', titleBadge, description, data }) => { return ( <CardContainer> - <Header> - <Title> - {title} - {/* NOT undefined, because we should allow zero (0) values */} - {titleBadge !== undefined && <Badge label={titleBadge} />} - </Title> - {!!description && <Description>{description}</Description>} - </Header> + {!!title || !!description ? ( + <Header> + {!!title && ( + <Title> + {title} + {/* NOT undefined, because we should allow zero (0) values */} + {titleBadge !== undefined && <Badge label={titleBadge} />} + </Title> + )} + + {!!description && <Description>{description}</Description>} + </Header> + ) : null} <DataCardFields data={data} /> </CardContainer> diff --git a/frontend/webapp/reuseable-components/icon-button/index.tsx b/frontend/webapp/reuseable-components/icon-button/index.tsx new file mode 100644 index 000000000..07e803bc2 --- /dev/null +++ b/frontend/webapp/reuseable-components/icon-button/index.tsx @@ -0,0 +1,63 @@ +import React, { CSSProperties, PropsWithChildren } from 'react'; +import styled, { keyframes } from 'styled-components'; + +interface Props extends PropsWithChildren { + onClick?: () => void; + withPing?: boolean; + pingColor?: CSSProperties['backgroundColor']; +} + +const Button = styled.button` + position: relative; + width: 36px; + height: 36px; + border: none; + border-radius: 100%; + background-color: transparent; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + &:hover { + background-color: ${({ theme }) => theme.colors.white_opacity['008']}; + } +`; + +const pingAnimation = keyframes` + 0% { + transform: scale(1); + opacity: 1; + } + 75%, 100% { + transform: scale(2); + opacity: 0; + } +`; + +const Ping = styled.div<{ $color: Props['pingColor'] }>` + position: absolute; + top: 8px; + right: 8px; + width: 6px; + height: 6px; + border-radius: 100%; + background-color: ${({ theme, $color }) => $color || theme.colors.secondary}; + + &::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 100%; + background-color: ${({ theme, $color }) => $color || theme.colors.secondary}; + animation: ${pingAnimation} 1.5s cubic-bezier(0, 0, 0.2, 1) infinite; + } +`; + +export const IconButton: React.FC<Props> = ({ children, onClick, withPing, pingColor }) => { + return ( + <Button onClick={onClick}> + {withPing && <Ping $color={pingColor} />} + {children} + </Button> + ); +}; diff --git a/frontend/webapp/reuseable-components/index.ts b/frontend/webapp/reuseable-components/index.ts index 1e2240a25..b160299d0 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -37,3 +37,4 @@ export * from './condition-details'; export * from './data-card'; export * from './data-tab'; export * from './code'; +export * from './icon-button'; diff --git a/frontend/webapp/store/useDrawerStore.ts b/frontend/webapp/store/useDrawerStore.ts index a4efa3a3e..bef2ef993 100644 --- a/frontend/webapp/store/useDrawerStore.ts +++ b/frontend/webapp/store/useDrawerStore.ts @@ -1,19 +1,19 @@ -// drawerStore.ts import { create } from 'zustand'; import type { ActionDataParsed, ActualDestination, InstrumentationRuleSpec, K8sActualSource, OVERVIEW_ENTITY_TYPES, WorkloadId } from '@/types'; -type ItemType = OVERVIEW_ENTITY_TYPES; +export enum DRAWER_OTHER_TYPES { + DESCRIBE_ODIGOS = 'describe-odigos', +} -export interface DrawerBaseItem { +export interface DrawerItem { + type: OVERVIEW_ENTITY_TYPES | DRAWER_OTHER_TYPES; id: string | WorkloadId; item?: InstrumentationRuleSpec | K8sActualSource | ActionDataParsed | ActualDestination; - type: ItemType; - // Add common properties here } interface DrawerStoreState { - selectedItem: DrawerBaseItem | null; - setSelectedItem: (item: DrawerBaseItem | null) => void; + selectedItem: DrawerItem | null; + setSelectedItem: (item: DrawerItem | null) => void; isDrawerOpen: boolean; openDrawer: () => void; closeDrawer: () => void; diff --git a/frontend/webapp/types/describe.ts b/frontend/webapp/types/describe.ts index 5aa3a8551..ead5c51e0 100644 --- a/frontend/webapp/types/describe.ts +++ b/frontend/webapp/types/describe.ts @@ -81,6 +81,46 @@ interface SourceAnalyze { pods: PodAnalyze[]; } +interface ClusterCollectorAnalyze { + enabled: EntityProperty; + collectorGroup: EntityProperty; + deployed?: EntityProperty; + deployedError?: EntityProperty; + collectorReady?: EntityProperty; + deploymentCreated: EntityProperty; + expectedReplicas?: EntityProperty; + healthyReplicas?: EntityProperty; + failedReplicas?: EntityProperty; + failedReplicasReason?: EntityProperty; +} + +interface NodeCollectorAnalyze { + enabled: EntityProperty; + collectorGroup: EntityProperty; + deployed?: EntityProperty; + deployedError?: EntityProperty; + collectorReady?: EntityProperty; + daemonSet: EntityProperty; + desiredNodes?: EntityProperty; + currentNodes?: EntityProperty; + updatedNodes?: EntityProperty; + availableNodes?: EntityProperty; +} + +interface OdigosAnalyze { + odigosVersion: EntityProperty; + numberOfDestinations: number; + numberOfSources: number; + clusterCollector: ClusterCollectorAnalyze; + nodeCollector: NodeCollectorAnalyze; + isSettled: boolean; + hasErrors: boolean; +} + export interface DescribeSource { describeSource: SourceAnalyze; } + +export interface DescribeOdigos { + describeOdigos: OdigosAnalyze; +} diff --git a/frontend/webapp/utils/constants/string.tsx b/frontend/webapp/utils/constants/string.tsx index 1eb13a917..4ccde2260 100644 --- a/frontend/webapp/utils/constants/string.tsx +++ b/frontend/webapp/utils/constants/string.tsx @@ -64,9 +64,10 @@ export const DATA_CARDS = { RULE_DETAILS: 'Instrumentation Rule Details', DESTINATION_DETAILS: 'Destination Details', SOURCE_DETAILS: 'Source Details', - DESCRIBE_SOURCE: 'Describe Source', DETECTED_CONTAINERS: 'Detected Containers', DETECTED_CONTAINERS_DESCRIPTION: 'The system automatically instruments the containers it detects with a supported programming language.', + DESCRIBE_SOURCE: 'Describe Source', + DESCRIBE_ODIGOS: 'Describe Odigos', }; export const DISPLAY_TITLES = {