diff --git a/cli/cmd/resources/ui.go b/cli/cmd/resources/ui.go index 5e416d0c6..00b4f86df 100644 --- a/cli/cmd/resources/ui.go +++ b/cli/cmd/resources/ui.go @@ -250,7 +250,7 @@ func NewUIClusterRole() *rbacv1.ClusterRole { }, { APIGroups: []string{"apps"}, - Resources: []string{"deployments", "statefulsets", "daemonsets"}, + Resources: []string{"deployments", "statefulsets", "daemonsets", "replicasets"}, Verbs: []string{"get", "list", "watch", "patch", "update"}, }, { diff --git a/frontend/webapp/components/main/header/index.tsx b/frontend/webapp/components/main/header/index.tsx index 188732d39..b4ddeb2c2 100644 --- a/frontend/webapp/components/main/header/index.tsx +++ b/frontend/webapp/components/main/header/index.tsx @@ -13,7 +13,7 @@ interface MainHeaderProps {} const HeaderContainer = styled(FlexRow)` width: 100%; padding: 12px 0; - background-color: ${({ theme }) => theme.colors.darker_grey}; + background-color: ${({ theme }) => theme.colors.dark_grey}; border-bottom: 1px solid rgba(249, 249, 249, 0.16); `; diff --git a/frontend/webapp/components/overview/all-drawers/describe-drawer.tsx b/frontend/webapp/components/overview/all-drawers/describe-drawer.tsx index 4b893587f..4b5d3b87f 100644 --- a/frontend/webapp/components/overview/all-drawers/describe-drawer.tsx +++ b/frontend/webapp/components/overview/all-drawers/describe-drawer.tsx @@ -24,6 +24,7 @@ export const DescribeDrawer: React.FC = () => { data={[ { type: DataCardFieldTypes.CODE, + width: 'inherit', value: JSON.stringify({ language: 'json', code: safeJsonStringify(describe) }), }, ]} diff --git a/frontend/webapp/components/setup/header/index.tsx b/frontend/webapp/components/setup/header/index.tsx index eaf1b3bc4..0c22d70ba 100644 --- a/frontend/webapp/components/setup/header/index.tsx +++ b/frontend/webapp/components/setup/header/index.tsx @@ -18,7 +18,7 @@ const HeaderContainer = styled.div` justify-content: space-between; padding: 0 24px 0 32px; align-items: center; - background-color: ${({ theme }) => theme.colors.darker_grey}; + background-color: ${({ theme }) => theme.colors.dark_grey}; border-bottom: 1px solid rgba(249, 249, 249, 0.16); height: 80px; `; diff --git a/frontend/webapp/containers/main/actions/action-drawer/index.tsx b/frontend/webapp/containers/main/actions/action-drawer/index.tsx index d0b0936e7..fe0d9c893 100644 --- a/frontend/webapp/containers/main/actions/action-drawer/index.tsx +++ b/frontend/webapp/containers/main/actions/action-drawer/index.tsx @@ -3,10 +3,10 @@ import buildCard from './build-card'; import { ActionFormBody } from '../'; import styled from 'styled-components'; import { useDrawerStore } from '@/store'; -import { ACTION, DATA_CARDS, getActionIcon } from '@/utils'; import buildDrawerItem from './build-drawer-item'; import { DataCard } from '@/reuseable-components'; import { useActionCRUD, useActionFormData } from '@/hooks'; +import { ACTION, DATA_CARDS, getActionIcon } from '@/utils'; import OverviewDrawer from '../../overview/overview-drawer'; import { ACTION_OPTIONS } from '../action-modal/action-options'; import { OVERVIEW_ENTITY_TYPES, type ActionDataParsed } from '@/types'; diff --git a/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx index 0991e1761..06b2086c6 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx @@ -5,7 +5,7 @@ import { extractMonitors } from '@/utils'; import { DeleteWarning } from '@/components'; import { IAppState, useAppStore } from '@/store'; import { OVERVIEW_ENTITY_TYPES, type ConfiguredDestination } from '@/types'; -import { DataCardFields, DataTab, IconButton, Text } from '@/reuseable-components'; +import { DataCardFields, DataTab, IconButton } from '@/reuseable-components'; const Container = styled.div` display: flex; @@ -27,7 +27,6 @@ const ListItem: React.FC<{ item: ConfiguredDestination; isLastItem: boolean }> = <> theme.font_family.secondary}; - text-transform: uppercase; - margin-right: 16px; -`; - -interface DestinationListItemProps { - item: DestinationTypeItem; - onSelect: (item: DestinationTypeItem) => void; -} - -export const DestinationListItem: React.FC = ({ item, onSelect }) => { - const renderSupportedSignals = () => { - const signals = Object.keys(item.supportedSignals).filter((signal) => item.supportedSignals[signal].supported); - - return signals.map((signal, index) => ( - - {signal} - {index < signals.length - 1 && ยท} - - )); - }; - - return ( - onSelect(item)}> - - - destination - - - {item.displayName} - {renderSupportedSignals()} - - - - {'Select'} - - - ); -}; diff --git a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx index 41bb135ec..c3ce504b4 100644 --- a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx @@ -3,14 +3,14 @@ import styled from 'styled-components'; import { DestinationTypeItem } from '@/types'; import { IDestinationListItem } from '@/hooks'; import { capitalizeFirstLetter } from '@/utils'; -import { DestinationListItem } from './destination-list-item'; -import { NoDataFound, SectionTitle } from '@/reuseable-components'; +import { DataTab, NoDataFound, SectionTitle } from '@/reuseable-components'; import { PotentialDestinationsList } from './potential-destinations-list'; const Container = styled.div` display: flex; flex-direction: column; align-self: stretch; + gap: 24px; max-height: calc(100vh - 450px); overflow-y: auto; @@ -44,12 +44,21 @@ const DestinationsList: React.FC = ({ items, setSelectedI ); } - return items.map((item) => { + return items.map((categoryItem) => { return ( - - - {item.items.map((categoryItem) => ( - + + + {categoryItem.items.map((destinationItem) => ( + destinationItem.supportedSignals[signal].supported)} + monitorsWithLabels + onClick={() => setSelectedItems(destinationItem)} + /> ))} ); diff --git a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx index d52c75038..3fec4a5cc 100644 --- a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx @@ -2,8 +2,11 @@ import React from 'react'; import styled from 'styled-components'; import { DestinationTypeItem } from '@/types'; import { usePotentialDestinations } from '@/hooks'; -import { DestinationListItem } from '../destination-list-item'; -import { SectionTitle, SkeletonLoader } from '@/reuseable-components'; +import { DataTab, SectionTitle, SkeletonLoader } from '@/reuseable-components'; + +interface Props { + setSelectedItems: (item: DestinationTypeItem) => void; +} const ListsWrapper = styled.div` display: flex; @@ -11,14 +14,10 @@ const ListsWrapper = styled.div` gap: 12px; `; -interface PotentialDestinationsListProps { - setSelectedItems: (item: DestinationTypeItem) => void; -} - -export const PotentialDestinationsList: React.FC = ({ setSelectedItems }) => { - const { loading, data } = usePotentialDestinations(); +export const PotentialDestinationsList: React.FC = ({ setSelectedItems }) => { + const { data, loading } = usePotentialDestinations(); - if (!data.length) return null; + if (!data.length && !loading) return null; return ( @@ -28,7 +27,22 @@ export const PotentialDestinationsList: React.FC title='Detected by Odigos' description='Odigos detects destinations for which automatic connection is available. All data will be filled out automatically.' /> - {loading ? : data.map((item) => )} + {loading ? ( + + ) : ( + data.map((item) => ( + item.supportedSignals[signal].supported)} + monitorsWithLabels + onClick={() => setSelectedItems(item)} + /> + )) + )} ); }; diff --git a/frontend/webapp/containers/main/overview/overview-data-flow/build-action-nodes.ts b/frontend/webapp/containers/main/overview/overview-data-flow/build-action-nodes.ts index 116d8b481..373c532f0 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/build-action-nodes.ts +++ b/frontend/webapp/containers/main/overview/overview-data-flow/build-action-nodes.ts @@ -6,6 +6,7 @@ import { getActionIcon, getEntityIcon, getEntityLabel } from '@/utils'; import { NODE_TYPES, OVERVIEW_ENTITY_TYPES, OVERVIEW_NODE_TYPES, STATUSES, type ComputePlatformMapped } from '@/types'; interface Params { + loading: boolean; entities: ComputePlatformMapped['computePlatform']['actions']; positions: NodePositions; unfilteredCounts: EntityCounts; @@ -28,7 +29,7 @@ const mapToNodeData = (entity: Params['entities'][0]) => { }; }; -export const buildActionNodes = ({ entities, positions, unfilteredCounts }: Params) => { +export const buildActionNodes = ({ loading, entities, positions, unfilteredCounts }: Params) => { const nodes: Node[] = []; const position = positions[OVERVIEW_ENTITY_TYPES.ACTION]; const unfilteredCount = unfilteredCounts[OVERVIEW_ENTITY_TYPES.ACTION]; @@ -48,7 +49,20 @@ export const buildActionNodes = ({ entities, positions, unfilteredCounts }: Para }, }); - if (!entities.length) { + if (loading) { + nodes.push({ + id: 'action-skeleton', + type: NODE_TYPES.SKELETON, + position: { + x: position['x'], + y: position['y'](), + }, + data: { + nodeWidth, + size: 3, + }, + }); + } else if (!entities.length) { nodes.push({ id: 'action-add', type: NODE_TYPES.ADD, diff --git a/frontend/webapp/containers/main/overview/overview-data-flow/build-destination-nodes.ts b/frontend/webapp/containers/main/overview/overview-data-flow/build-destination-nodes.ts index 8d7c6dfd6..9f4ceaeec 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/build-destination-nodes.ts +++ b/frontend/webapp/containers/main/overview/overview-data-flow/build-destination-nodes.ts @@ -6,6 +6,7 @@ import { extractMonitors, getEntityIcon, getEntityLabel, getHealthStatus } from import { NODE_TYPES, OVERVIEW_ENTITY_TYPES, OVERVIEW_NODE_TYPES, STATUSES, type ComputePlatformMapped } from '@/types'; interface Params { + loading: boolean; entities: ComputePlatformMapped['computePlatform']['destinations']; positions: NodePositions; unfilteredCounts: EntityCounts; @@ -27,7 +28,7 @@ const mapToNodeData = (entity: Params['entities'][0]) => { }; }; -export const buildDestinationNodes = ({ entities, positions, unfilteredCounts }: Params) => { +export const buildDestinationNodes = ({ loading, entities, positions, unfilteredCounts }: Params) => { const nodes: Node[] = []; const position = positions[OVERVIEW_ENTITY_TYPES.DESTINATION]; const unfilteredCount = unfilteredCounts[OVERVIEW_ENTITY_TYPES.DESTINATION]; @@ -47,7 +48,20 @@ export const buildDestinationNodes = ({ entities, positions, unfilteredCounts }: }, }); - if (!entities.length) { + if (loading) { + nodes.push({ + id: 'destination-skeleton', + type: NODE_TYPES.SKELETON, + position: { + x: position['x'], + y: position['y'](), + }, + data: { + nodeWidth, + size: 3, + }, + }); + } else if (!entities.length) { nodes.push({ id: 'destination-add', type: NODE_TYPES.ADD, diff --git a/frontend/webapp/containers/main/overview/overview-data-flow/build-rule-nodes.ts b/frontend/webapp/containers/main/overview/overview-data-flow/build-rule-nodes.ts index dd6f05c42..5caa9e7c5 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/build-rule-nodes.ts +++ b/frontend/webapp/containers/main/overview/overview-data-flow/build-rule-nodes.ts @@ -6,6 +6,7 @@ import { getEntityIcon, getEntityLabel, getRuleIcon } from '@/utils'; import { NODE_TYPES, OVERVIEW_ENTITY_TYPES, OVERVIEW_NODE_TYPES, STATUSES, type ComputePlatformMapped } from '@/types'; interface Params { + loading: boolean; entities: ComputePlatformMapped['computePlatform']['instrumentationRules']; positions: NodePositions; unfilteredCounts: EntityCounts; @@ -27,7 +28,7 @@ const mapToNodeData = (entity: Params['entities'][0]) => { }; }; -export const buildRuleNodes = ({ entities, positions, unfilteredCounts }: Params) => { +export const buildRuleNodes = ({ loading, entities, positions, unfilteredCounts }: Params) => { const nodes: Node[] = []; const position = positions[OVERVIEW_ENTITY_TYPES.RULE]; const unfilteredCount = unfilteredCounts[OVERVIEW_ENTITY_TYPES.RULE]; @@ -47,7 +48,20 @@ export const buildRuleNodes = ({ entities, positions, unfilteredCounts }: Params }, }); - if (!entities.length) { + if (loading) { + nodes.push({ + id: 'rule-skeleton', + type: NODE_TYPES.SKELETON, + position: { + x: position['x'], + y: position['y'](), + }, + data: { + nodeWidth, + size: 3, + }, + }); + } else if (!entities.length) { nodes.push({ id: 'rule-add', type: NODE_TYPES.ADD, diff --git a/frontend/webapp/containers/main/overview/overview-data-flow/build-source-nodes.ts b/frontend/webapp/containers/main/overview/overview-data-flow/build-source-nodes.ts index c4a3c67f7..26301a422 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/build-source-nodes.ts +++ b/frontend/webapp/containers/main/overview/overview-data-flow/build-source-nodes.ts @@ -7,10 +7,10 @@ import { getEntityIcon, getEntityLabel, getHealthStatus, getProgrammingLanguageI import { NODE_TYPES, OVERVIEW_ENTITY_TYPES, OVERVIEW_NODE_TYPES, STATUSES, type ComputePlatformMapped } from '@/types'; interface Params { + loading: boolean; entities: ComputePlatformMapped['computePlatform']['k8sActualSources']; positions: NodePositions; unfilteredCounts: EntityCounts; - containerHeight: number; onScroll: (params: { clientHeight: number; scrollHeight: number; scrollTop: number }) => void; } @@ -36,7 +36,7 @@ const mapToNodeData = (entity: Params['entities'][0]) => { }; }; -export const buildSourceNodes = ({ entities, positions, unfilteredCounts, containerHeight, onScroll }: Params) => { +export const buildSourceNodes = ({ loading, entities, positions, unfilteredCounts, containerHeight, onScroll }: Params) => { const nodes: Node[] = []; const position = positions[OVERVIEW_ENTITY_TYPES.SOURCE]; const unfilteredCount = unfilteredCounts[OVERVIEW_ENTITY_TYPES.SOURCE]; @@ -56,7 +56,20 @@ export const buildSourceNodes = ({ entities, positions, unfilteredCounts, contai }, }); - if (!entities.length) { + if (loading) { + nodes.push({ + id: 'source-skeleton', + type: NODE_TYPES.SKELETON, + position: { + x: position['x'], + y: position['y'](), + }, + data: { + nodeWidth, + size: 3, + }, + }); + } else if (!entities.length) { nodes.push({ id: 'source-add', type: NODE_TYPES.ADD, 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 e4198e67c..77acdda75 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx @@ -35,31 +35,50 @@ export default function OverviewDataFlowContainer() { const positions = useMemo(() => getNodePositions({ containerWidth }), [containerWidth]); const { metrics } = useMetrics(); - const { data, filteredData } = useComputePlatform(); + const { data, filteredData, loading } = useComputePlatform(); const unfilteredCounts = useMemo(() => getEntityCounts({ computePlatform: data?.computePlatform }), [data]); const ruleNodes = useMemo( - () => buildRuleNodes({ entities: filteredData?.computePlatform.instrumentationRules || [], positions, unfilteredCounts }), - [filteredData?.computePlatform.instrumentationRules, positions, unfilteredCounts], + () => + buildRuleNodes({ + loading, + entities: filteredData?.computePlatform.instrumentationRules || [], + positions, + unfilteredCounts, + }), + [loading, filteredData?.computePlatform.instrumentationRules, positions, unfilteredCounts], ); const actionNodes = useMemo( - () => buildActionNodes({ entities: filteredData?.computePlatform.actions || [], positions, unfilteredCounts }), - [filteredData?.computePlatform.actions, positions, unfilteredCounts], + () => + buildActionNodes({ + loading, + entities: filteredData?.computePlatform.actions || [], + positions, + unfilteredCounts, + }), + [loading, filteredData?.computePlatform.actions, positions, unfilteredCounts], ); const destinationNodes = useMemo( - () => buildDestinationNodes({ entities: filteredData?.computePlatform.destinations || [], positions, unfilteredCounts }), - [filteredData?.computePlatform.destinations, positions, unfilteredCounts], + () => + buildDestinationNodes({ + loading, + entities: filteredData?.computePlatform.destinations || [], + positions, + unfilteredCounts, + }), + [loading, filteredData?.computePlatform.destinations, positions, unfilteredCounts], ); const sourceNodes = useMemo( () => buildSourceNodes({ + loading, entities: filteredData?.computePlatform.k8sActualSources || [], positions, unfilteredCounts, containerHeight, onScroll: ({ scrollTop }) => setScrollYOffset(scrollTop), }), - [filteredData?.computePlatform.k8sActualSources, positions, unfilteredCounts, containerHeight], + [loading, filteredData?.computePlatform.k8sActualSources, positions, unfilteredCounts, containerHeight], ); const [nodes, setNodes, onNodesChange] = useNodesState(([] as Node[]).concat(actionNodes, ruleNodes, sourceNodes, destinationNodes)); 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 c7a78f08a..ed75e91c8 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 @@ -1,7 +1,7 @@ import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react'; import Image from 'next/image'; import styled from 'styled-components'; -import { Button, Input, Text, Tooltip } from '@/reuseable-components'; +import { Button, IconWrapped, Input, Text, Tooltip } from '@/reuseable-components'; const HeaderContainer = styled.section` display: flex; @@ -32,17 +32,6 @@ const Title = styled(Text)` text-overflow: ellipsis; `; -const DrawerItemImageWrapper = styled.div` - display: flex; - width: 36px; - height: 36px; - justify-content: center; - align-items: center; - gap: 8px; - border-radius: 8px; - background: linear-gradient(180deg, rgba(249, 249, 249, 0.06) 0%, rgba(249, 249, 249, 0.02) 100%); -`; - const EditButton = styled(Button)` gap: 8px; `; @@ -87,11 +76,7 @@ const DrawerHeader = forwardRef(({ title, ti return ( - {!!imageUri && ( - - Drawer Item - - )} + {!!imageUri && } {!isEdit && ( diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 650187ac4..35c8210f2 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -13,7 +13,7 @@ const DRAWER_WIDTH = `${640 + 64}px`; // +64 because of "ContentArea" padding interface Props { title: string; titleTooltip?: string; - imageUri: string; + imageUri?: string; isEdit?: boolean; isFormDirty?: boolean; onEdit?: (bool?: boolean) => void; diff --git a/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx b/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx index 5a86fbf4e..0c5a0fd82 100644 --- a/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx @@ -7,8 +7,8 @@ import { UpdateSourceBody } from '../update-source-body'; import { useDescribeSource, useSourceCRUD } from '@/hooks'; import OverviewDrawer from '../../overview/overview-drawer'; import { OVERVIEW_ENTITY_TYPES, type WorkloadId, type K8sActualSource } from '@/types'; +import { ACTION, BACKEND_BOOLEAN, DATA_CARDS, getEntityIcon, safeJsonStringify } from '@/utils'; import { ConditionDetails, DataCard, DataCardRow, DataCardFieldTypes } from '@/reuseable-components'; -import { ACTION, BACKEND_BOOLEAN, DATA_CARDS, getMainContainerLanguage, getProgrammingLanguageIcon, safeJsonStringify } from '@/utils'; interface Props {} @@ -123,7 +123,7 @@ export const SourceDrawer: React.FC = () => { = () => { data={[ { type: DataCardFieldTypes.CODE, + width: 'inherit', value: JSON.stringify({ language: 'json', code: safeJsonStringify(describe) }), }, ]} diff --git a/frontend/webapp/reuseable-components/data-card/data-card-fields/index.tsx b/frontend/webapp/reuseable-components/data-card/data-card-fields/index.tsx index cfdfb8ceb..764960676 100644 --- a/frontend/webapp/reuseable-components/data-card/data-card-fields/index.tsx +++ b/frontend/webapp/reuseable-components/data-card/data-card-fields/index.tsx @@ -1,8 +1,8 @@ import React, { useId } from 'react'; import styled from 'styled-components'; +import { NOTIFICATION_TYPE } from '@/types'; import { ActiveStatus, Code, DataTab, Divider, InstrumentStatus, MonitorsIcons, NotificationNote, Text, Tooltip } from '@/reuseable-components'; import { capitalizeFirstLetter, getProgrammingLanguageIcon, parseJsonStringToPrettyString, safeJsonParse, WORKLOAD_PROGRAMMING_LANGUAGES } from '@/utils'; -import { NOTIFICATION_TYPE } from '@/types'; export enum DataCardFieldTypes { DIVIDER = 'divider', @@ -47,7 +47,7 @@ const ItemTitle = styled(Text)` export const DataCardFields: React.FC = ({ data }) => { return ( - {data.map(({ type, title, tooltip, value, width = 'inherit' }) => { + {data.map(({ type, title, tooltip, value, width = 'unset' }) => { const id = useId(); return ( diff --git a/frontend/webapp/reuseable-components/data-tab/index.tsx b/frontend/webapp/reuseable-components/data-tab/index.tsx index 1fa6cbf4b..c817e9693 100644 --- a/frontend/webapp/reuseable-components/data-tab/index.tsx +++ b/frontend/webapp/reuseable-components/data-tab/index.tsx @@ -1,13 +1,13 @@ import React, { Fragment, useCallback, useState } from 'react'; -import Image from 'next/image'; import { FlexColumn, FlexRow } from '@/styles'; import styled, { css } from 'styled-components'; -import { ActiveStatus, Divider, ExtendIcon, IconButton, MonitorsIcons, Text } from '@/reuseable-components'; +import { ActiveStatus, Divider, ExtendIcon, IconButton, IconWrapped, MonitorsIcons, Text } from '@/reuseable-components'; interface Props { title: string; - subTitle: string; + subTitle?: string; logo: string; + hoverText?: string; monitors?: string[]; monitorsWithLabels?: boolean; isActive?: boolean; @@ -19,6 +19,10 @@ interface Props { onClick?: () => void; } +const ControlledVisibility = styled.div` + visibility: hidden; +`; + const Container = styled.div<{ $withClick: boolean; $isError: Props['isError'] }>` display: flex; flex-direction: column; @@ -34,20 +38,17 @@ const Container = styled.div<{ $withClick: boolean; $isError: Props['isError'] } &:hover { cursor: pointer; background-color: ${$isError ? '#351515' : theme.colors.white_opacity['008']}; + ${ControlledVisibility} { + visibility: visible; + } } `} -`; -const IconWrapper = styled.div<{ $isError: Props['isError'] }>` - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 36px; - height: 36px; - border-radius: 8px; - background: ${({ $isError }) => - `linear-gradient(180deg, ${$isError ? 'rgba(237, 124, 124, 0.08)' : 'rgba(249, 249, 249, 0.06)'} 0%, ${$isError ? 'rgba(237, 124, 124, 0.02)' : 'rgba(249, 249, 249, 0.02)'} 100%)`}; + &:hover { + ${ControlledVisibility} { + visibility: visible; + } + } `; const Title = styled(Text)` @@ -76,7 +77,26 @@ const ActionsWrapper = styled.div` margin-left: auto; `; -export const DataTab: React.FC = ({ title, subTitle, logo, monitors, monitorsWithLabels, isActive, isError, withExtend, isExtended, renderExtended, renderActions, onClick }) => { +const HoverText = styled(Text)` + margin-right: 16px; +`; + +export const DataTab: React.FC = ({ + title, + subTitle, + logo, + hoverText, + monitors, + monitorsWithLabels, + isActive, + isError, + withExtend, + isExtended, + renderExtended, + renderActions, + onClick, + ...props +}) => { const [extend, setExtend] = useState(isExtended || false); const renderMonitors = useCallback( @@ -108,11 +128,9 @@ export const DataTab: React.FC = ({ title, subTitle, logo, monitors, moni ); return ( - + - - - + {title} @@ -124,6 +142,13 @@ export const DataTab: React.FC = ({ title, subTitle, logo, monitors, moni + {!!hoverText && ( + + + {hoverText} + + + )} {renderActions && renderActions()} {withExtend && ( diff --git a/frontend/webapp/reuseable-components/icon-wrapped/index.tsx b/frontend/webapp/reuseable-components/icon-wrapped/index.tsx new file mode 100644 index 000000000..53bfe2031 --- /dev/null +++ b/frontend/webapp/reuseable-components/icon-wrapped/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import Image from 'next/image'; +import styled from 'styled-components'; + +interface Props { + src: string; + alt?: string; + isError?: boolean; +} + +const Container = styled.div<{ $isError: Props['isError'] }>` + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 36px; + height: 36px; + border-radius: 8px; + background: ${({ $isError }) => + $isError ? 'linear-gradient(180deg, rgba(237, 124, 124, 0.2) 0%, rgba(237, 124, 124, 0.05) 100%);' : 'linear-gradient(180deg, rgba(249, 249, 249, 0.2) 0%, rgba(249, 249, 249, 0.05) 100%);'}; +`; + +export const IconWrapped: React.FC = ({ src, alt = '', isError }) => { + return ( + + {alt} + + ); +}; diff --git a/frontend/webapp/reuseable-components/index.ts b/frontend/webapp/reuseable-components/index.ts index b160299d0..e0a2e3346 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -38,3 +38,4 @@ export * from './data-card'; export * from './data-tab'; export * from './code'; export * from './icon-button'; +export * from './icon-wrapped'; diff --git a/frontend/webapp/reuseable-components/monitors-icons/index.tsx b/frontend/webapp/reuseable-components/monitors-icons/index.tsx index ccd8ae1fa..8616259a1 100644 --- a/frontend/webapp/reuseable-components/monitors-icons/index.tsx +++ b/frontend/webapp/reuseable-components/monitors-icons/index.tsx @@ -21,7 +21,7 @@ export const MonitorsIcons: React.FC = ({ monitors, withTooltips, withLab return ( - + {signal} {withLabels && ( diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx b/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx index 1a8edaa32..b1fef9d64 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx +++ b/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx @@ -8,6 +8,7 @@ import FrameNode from './nodes/frame-node'; import ScrollNode from './nodes/scroll-node'; import HeaderNode from './nodes/header-node'; import LabeledEdge from './edges/labeled-edge'; +import SkeletonNode from './nodes/skeleton-node'; import { EDGE_TYPES, NODE_TYPES } from '@/types'; import { Controls, type Edge, type Node, type OnEdgesChange, type OnNodesChange, ReactFlow } from '@xyflow/react'; @@ -49,6 +50,7 @@ const nodeTypes = { [NODE_TYPES.EDGED]: EdgedNode, [NODE_TYPES.FRAME]: FrameNode, [NODE_TYPES.SCROLL]: ScrollNode, + [NODE_TYPES.SKELETON]: SkeletonNode, }; const edgeTypes = { diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/skeleton-node.tsx b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/skeleton-node.tsx new file mode 100644 index 000000000..49a484329 --- /dev/null +++ b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/skeleton-node.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { NODE_TYPES } from '@/types'; +import styled from 'styled-components'; +import { type Node, type NodeProps } from '@xyflow/react'; +import { SkeletonLoader } from '@/reuseable-components/skeleton-loader'; + +interface Props + extends NodeProps< + Node< + { + nodeWidth: number; + size: number; + }, + NODE_TYPES.SKELETON + > + > {} + +const Container = styled.div<{ $nodeWidth: Props['data']['nodeWidth'] }>` + width: ${({ $nodeWidth }) => `${$nodeWidth}px`}; +`; + +const SkeletonNode: React.FC = ({ id: nodeId, data }) => { + const { nodeWidth, size } = data; + + return ( + + + + ); +}; + +export default SkeletonNode; diff --git a/frontend/webapp/reuseable-components/skeleton-loader/index.tsx b/frontend/webapp/reuseable-components/skeleton-loader/index.tsx index be6b1fb56..fac3fd26a 100644 --- a/frontend/webapp/reuseable-components/skeleton-loader/index.tsx +++ b/frontend/webapp/reuseable-components/skeleton-loader/index.tsx @@ -1,64 +1,63 @@ import React from 'react'; +import { FlexColumn } from '@/styles'; import styled, { keyframes } from 'styled-components'; -const shimmer = keyframes` +const shimmer = keyframes<{ $width: string }>` 0% { - background-position: -1000px 0; + background-position: -500px 0; } 100% { - background-position: 1000px 0; + background-position: 500px 0; } `; -const SkeletonLoaderWrapper = styled.div` +const Container = styled.div` display: flex; flex-direction: column; - gap: 1rem; + gap: 16px; `; const SkeletonItem = styled.div` display: flex; align-items: center; - gap: 1rem; + gap: 16px; `; -const SkeletonThumbnail = styled.div` +const Thumbnail = styled.div` width: 50px; height: 50px; border-radius: 8px; - background: ${({ theme }) => `linear-gradient(90deg, ${theme.colors.primary} 25%, ${theme.colors.primary} 50%, ${theme.colors.darker_grey} 75%)`}; + background: ${({ theme }) => `linear-gradient(90deg, ${theme.colors.dropdown_bg_2} 25%, ${theme.colors.dropdown_bg_2} 50%, ${theme.colors.border} 75%)`}; background-size: 200% 100%; animation: ${shimmer} 10s infinite linear; `; -const SkeletonText = styled.div` +const LineWrapper = styled(FlexColumn)` flex: 1; + gap: 12px; `; -const SkeletonLine = styled.div<{ $width: string }>` +const Line = styled.div<{ $width: string }>` + width: ${({ $width }) => $width}; height: 16px; - margin-bottom: 0.5rem; - background: ${({ theme }) => `linear-gradient(90deg, ${theme.colors.primary} 25%, ${theme.colors.primary} 50%, ${theme.colors.darker_grey} 75%)`}; + background: ${({ theme }) => `linear-gradient(90deg, ${theme.colors.dropdown_bg_2} 25%, ${theme.colors.dropdown_bg_2} 50%, ${theme.colors.border} 75%)`}; background-size: 200% 100%; animation: ${shimmer} 1.5s infinite linear; - width: ${({ $width }) => $width}; border-radius: 4px; `; -const SkeletonLoader: React.FC<{ size: number }> = ({ size = 5 }) => { +export const SkeletonLoader: React.FC<{ size?: number }> = ({ size = 5 }) => { return ( - + {[...Array(size)].map((_, index) => ( - - - - - + + + + + ))} - + ); }; - -export { SkeletonLoader }; diff --git a/frontend/webapp/types/data-flow.ts b/frontend/webapp/types/data-flow.ts index a2892e241..dcd65b74e 100644 --- a/frontend/webapp/types/data-flow.ts +++ b/frontend/webapp/types/data-flow.ts @@ -5,6 +5,7 @@ export enum NODE_TYPES { EDGED = 'edged', FRAME = 'frame', SCROLL = 'scroll', + SKELETON = 'skeleton', } export enum EDGE_TYPES { diff --git a/helm/odigos/templates/ui/clusterrole.yaml b/helm/odigos/templates/ui/clusterrole.yaml index 80a07c803..cb7992b83 100644 --- a/helm/odigos/templates/ui/clusterrole.yaml +++ b/helm/odigos/templates/ui/clusterrole.yaml @@ -43,6 +43,7 @@ rules: - deployments - statefulsets - daemonsets + - replicasets verbs: - get - list