diff --git a/frontend/webapp/app/globals.css b/frontend/webapp/app/globals.css index 302fd6694..37f548102 100644 --- a/frontend/webapp/app/globals.css +++ b/frontend/webapp/app/globals.css @@ -2,6 +2,7 @@ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Kode+Mono:wght@100;200;300;400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap'); * { scrollbar-color: black transparent; diff --git a/frontend/webapp/app/layout.tsx b/frontend/webapp/app/layout.tsx index 62d808b5d..f0254a8b5 100644 --- a/frontend/webapp/app/layout.tsx +++ b/frontend/webapp/app/layout.tsx @@ -20,7 +20,6 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( - {METADATA.title} @@ -28,6 +27,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) + {METADATA.title} 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 6f9381e93..b1feeeb42 100644 --- a/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx @@ -1,14 +1,14 @@ import React, { useEffect, useMemo, useState } from 'react'; import buildCard from './build-card'; import styled from 'styled-components'; -import { useSourceCRUD } from '@/hooks'; import { useDrawerStore } from '@/store'; import buildDrawerItem from './build-drawer-item'; 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, DATA_CARDS, getMainContainerLanguage, getProgrammingLanguageIcon } from '@/utils'; import { ConditionDetails, DataCard, DataCardRow, DataCardFieldTypes } from '@/reuseable-components'; +import { ACTION, DATA_CARDS, getMainContainerLanguage, getProgrammingLanguageIcon, safeJsonStringify } from '@/utils'; interface Props {} @@ -93,6 +93,7 @@ export const SourceDrawer: React.FC = () => { if (!selectedItem?.item) return null; const { id, item } = selectedItem as { id: WorkloadId; item: K8sActualSource }; + const { data: describe } = useDescribeSource(id); const handleEdit = (bool?: boolean) => { setIsEditing(typeof bool === 'boolean' ? bool : true); @@ -141,6 +142,15 @@ export const SourceDrawer: React.FC = () => { + )} diff --git a/frontend/webapp/graphql/queries/describe.ts b/frontend/webapp/graphql/queries/describe.ts new file mode 100644 index 000000000..b2207ef9c --- /dev/null +++ b/frontend/webapp/graphql/queries/describe.ts @@ -0,0 +1,224 @@ +import { gql } from '@apollo/client'; + +export const DESCRIBE_SOURCE = gql` + query DescribeSource($namespace: String!, $kind: String!, $name: String!) { + describeSource(namespace: $namespace, kind: $kind, name: $name) { + name { + name + value + status + explain + } + kind { + name + value + status + explain + } + namespace { + name + value + status + explain + } + labels { + instrumented { + name + value + status + explain + } + workload { + name + value + status + explain + } + namespace { + name + value + status + explain + } + instrumentedText { + name + value + status + explain + } + } + instrumentationConfig { + created { + name + value + status + explain + } + createTime { + name + value + status + explain + } + } + runtimeInfo { + generation { + name + value + status + explain + } + containers { + containerName { + name + value + status + explain + } + language { + name + value + status + explain + } + runtimeVersion { + name + value + status + explain + } + envVars { + name + value + status + explain + } + } + } + instrumentedApplication { + created { + name + value + status + explain + } + createTime { + name + value + status + explain + } + containers { + containerName { + name + value + status + explain + } + language { + name + value + status + explain + } + runtimeVersion { + name + value + status + explain + } + envVars { + name + value + status + explain + } + } + } + instrumentationDevice { + statusText { + name + value + status + explain + } + containers { + containerName { + name + value + status + explain + } + devices { + name + value + status + explain + } + originalEnv { + name + value + status + explain + } + } + } + totalPods + podsPhasesCount + pods { + podName { + name + value + status + explain + } + nodeName { + name + value + status + explain + } + phase { + name + value + status + explain + } + containers { + containerName { + name + value + status + explain + } + actualDevices { + name + value + status + explain + } + instrumentationInstances { + healthy { + name + value + status + explain + } + message { + name + value + status + explain + } + identifyingAttributes { + name + value + status + explain + } + } + } + } + } + } +`; diff --git a/frontend/webapp/graphql/queries/index.ts b/frontend/webapp/graphql/queries/index.ts index 498bd90f5..a7c69d689 100644 --- a/frontend/webapp/graphql/queries/index.ts +++ b/frontend/webapp/graphql/queries/index.ts @@ -1,3 +1,4 @@ export * from './config'; export * from './compute-platform'; +export * from './describe'; export * from './destination'; diff --git a/frontend/webapp/hooks/describe/index.ts b/frontend/webapp/hooks/describe/index.ts new file mode 100644 index 000000000..0d183ae60 --- /dev/null +++ b/frontend/webapp/hooks/describe/index.ts @@ -0,0 +1 @@ +export * from './useDescribeSource'; diff --git a/frontend/webapp/hooks/describe/useDescribeSource.ts b/frontend/webapp/hooks/describe/useDescribeSource.ts new file mode 100644 index 000000000..209e92794 --- /dev/null +++ b/frontend/webapp/hooks/describe/useDescribeSource.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@apollo/client'; +import { DESCRIBE_SOURCE } from '@/graphql'; +import type { DescribeSource, WorkloadId } from '@/types'; + +export const useDescribeSource = ({ namespace, name, kind }: WorkloadId) => { + const { data, loading, error } = useQuery(DESCRIBE_SOURCE, { + variables: { namespace, name, kind }, + pollInterval: 5000, + }); + + return { + data: data?.describeSource, + loading, + error, + }; +}; diff --git a/frontend/webapp/hooks/index.tsx b/frontend/webapp/hooks/index.ts similarity index 90% rename from frontend/webapp/hooks/index.tsx rename to frontend/webapp/hooks/index.ts index 6198e2ae9..0996541ea 100644 --- a/frontend/webapp/hooks/index.tsx +++ b/frontend/webapp/hooks/index.ts @@ -1,9 +1,10 @@ +export * from './actions'; export * from './common'; +export * from './compute-platform'; export * from './config'; -export * from './sources'; -export * from './actions'; -export * from './overview'; -export * from './notification'; +export * from './describe'; export * from './destinations'; -export * from './compute-platform'; export * from './instrumentation-rules'; +export * from './notification'; +export * from './overview'; +export * from './sources'; diff --git a/frontend/webapp/package.json b/frontend/webapp/package.json index 9fdd896f0..39b8b08dd 100644 --- a/frontend/webapp/package.json +++ b/frontend/webapp/package.json @@ -18,6 +18,7 @@ "graphql": "^16.9.0", "javascript-time-ago": "^2.5.11", "next": "15.0.3", + "prism-react-renderer": "^2.4.1", "react": "18.3.1", "react-dom": "18.3.1", "react-flow-renderer": "^10.3.17", diff --git a/frontend/webapp/reuseable-components/code/index.tsx b/frontend/webapp/reuseable-components/code/index.tsx new file mode 100644 index 000000000..21d281f22 --- /dev/null +++ b/frontend/webapp/reuseable-components/code/index.tsx @@ -0,0 +1,37 @@ +import styled from 'styled-components'; +import { Highlight, themes as prismThemes } from 'prism-react-renderer'; +import { flattenObjectKeys, safeJsonParse, safeJsonStringify } from '@/utils'; + +interface Props { + language: string; + code: string; + flatten?: boolean; +} + +const Token = styled.span` + white-space: pre-wrap; + overflow-wrap: break-word; + opacity: 0.75; + font-size: 12px; + font-family: ${({ theme }) => theme.font_family.code}; +`; + +export const Code: React.FC = ({ language, code, flatten }) => { + const str = flatten && language === 'json' ? safeJsonStringify(flattenObjectKeys(safeJsonParse(code, {}))) : code; + + return ( + + {({ getLineProps, getTokenProps, tokens }) => ( +
+          {tokens.map((line, i) => (
+            
+ {line.map((token, ii) => ( + + ))} +
+ ))} +
+ )} +
+ ); +}; 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 dc449ea79..c09ce002e 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,6 +1,6 @@ import React, { useId } from 'react'; import styled from 'styled-components'; -import { ActiveStatus, DataTab, Divider, InstrumentStatus, MonitorsIcons, Text, Tooltip } from '@/reuseable-components'; +import { ActiveStatus, Code, DataTab, Divider, InstrumentStatus, MonitorsIcons, Text, Tooltip } from '@/reuseable-components'; import { capitalizeFirstLetter, getProgrammingLanguageIcon, parseJsonStringToPrettyString, safeJsonParse, WORKLOAD_PROGRAMMING_LANGUAGES } from '@/utils'; export enum DataCardFieldTypes { @@ -8,6 +8,7 @@ export enum DataCardFieldTypes { MONITORS = 'monitors', ACTIVE_STATUS = 'active-status', SOURCE_CONTAINER = 'source-container', + CODE = 'code', } export interface DataCardRow { @@ -45,7 +46,7 @@ const ItemTitle = styled(Text)` export const DataCardFields: React.FC = ({ data }) => { return ( - {data.map(({ type, title, tooltip, value, width = 'unset' }) => { + {data.map(({ type, title, tooltip, value, width = 'inherit' }) => { const id = useId(); return ( @@ -97,6 +98,12 @@ const renderValue = (type: DataCardRow['type'], value: DataCardRow['value']) => ); } + case DataCardFieldTypes.CODE: { + const params = safeJsonParse(value, { language: '', code: '' }); + + return ; + } + default: { return {parseJsonStringToPrettyString(value || '-')}; } diff --git a/frontend/webapp/reuseable-components/index.ts b/frontend/webapp/reuseable-components/index.ts index 6735fe776..1e2240a25 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -36,3 +36,4 @@ export * from './extend-icon'; export * from './condition-details'; export * from './data-card'; export * from './data-tab'; +export * from './code'; diff --git a/frontend/webapp/styles/theme.ts b/frontend/webapp/styles/theme.ts index 48fa07254..6e1e71cee 100644 --- a/frontend/webapp/styles/theme.ts +++ b/frontend/webapp/styles/theme.ts @@ -245,6 +245,7 @@ const text = { const font_family = { primary: 'Inter, sans-serif', secondary: 'Kode Mono, sans-serif', + code: 'IBM Plex Mono, monospace', }; // Define the theme interface diff --git a/frontend/webapp/types/describe.ts b/frontend/webapp/types/describe.ts new file mode 100644 index 000000000..5aa3a8551 --- /dev/null +++ b/frontend/webapp/types/describe.ts @@ -0,0 +1,86 @@ +interface EntityProperty { + name: string; + value: string; + status?: string; + explain?: string; +} + +interface InstrumentationLabelsAnalyze { + instrumented: EntityProperty; + workload?: EntityProperty; + namespace?: EntityProperty; + instrumentedText?: EntityProperty; +} + +interface InstrumentationConfigAnalyze { + created: EntityProperty; + createTime?: EntityProperty; +} + +interface ContainerRuntimeInfoAnalyze { + containerName: EntityProperty; + language: EntityProperty; + runtimeVersion: EntityProperty; + envVars: EntityProperty[]; +} + +interface RuntimeInfoAnalyze { + generation: EntityProperty; + containers: ContainerRuntimeInfoAnalyze[]; +} + +interface InstrumentedApplicationAnalyze { + created: EntityProperty; + createTime?: EntityProperty; + containers: ContainerRuntimeInfoAnalyze[]; +} + +interface ContainerWorkloadManifestAnalyze { + containerName: EntityProperty; + devices: EntityProperty; + originalEnv: EntityProperty[]; +} + +interface InstrumentationDeviceAnalyze { + statusText: EntityProperty; + containers: ContainerWorkloadManifestAnalyze[]; +} + +interface InstrumentationInstanceAnalyze { + healthy: EntityProperty; + message?: EntityProperty; + identifyingAttributes: EntityProperty[]; +} + +interface PodContainerAnalyze { + containerName: EntityProperty; + actualDevices: EntityProperty; + instrumentationInstances: InstrumentationInstanceAnalyze[]; +} + +interface PodAnalyze { + podName: EntityProperty; + nodeName: EntityProperty; + phase: EntityProperty; + containers: PodContainerAnalyze[]; +} + +interface SourceAnalyze { + name: EntityProperty; + kind: EntityProperty; + namespace: EntityProperty; + labels: InstrumentationLabelsAnalyze; + + instrumentationConfig: InstrumentationConfigAnalyze; + runtimeInfo?: RuntimeInfoAnalyze; + instrumentedApplication: InstrumentedApplicationAnalyze; + instrumentationDevice: InstrumentationDeviceAnalyze; + + totalPods: number; + podsPhasesCount: string; + pods: PodAnalyze[]; +} + +export interface DescribeSource { + describeSource: SourceAnalyze; +} diff --git a/frontend/webapp/types/index.ts b/frontend/webapp/types/index.ts index 1a5009a4f..4dee70aa1 100644 --- a/frontend/webapp/types/index.ts +++ b/frontend/webapp/types/index.ts @@ -2,6 +2,7 @@ export * from './actions'; export * from './common'; export * from './compute-platform'; export * from './data-flow'; +export * from './describe'; export * from './destinations'; export * from './instrumentation-rules'; export * from './metrics'; diff --git a/frontend/webapp/utils/constants/string.tsx b/frontend/webapp/utils/constants/string.tsx index 56a4ab275..1eb13a917 100644 --- a/frontend/webapp/utils/constants/string.tsx +++ b/frontend/webapp/utils/constants/string.tsx @@ -64,7 +64,7 @@ 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.', }; diff --git a/frontend/webapp/utils/functions/formatters/flatten-object-keys/index.ts b/frontend/webapp/utils/functions/formatters/flatten-object-keys/index.ts new file mode 100644 index 000000000..b89355214 --- /dev/null +++ b/frontend/webapp/utils/functions/formatters/flatten-object-keys/index.ts @@ -0,0 +1,62 @@ +/** + * Recursively flattens a nested object into a single-level object where each key + * represents the path to its corresponding value in the original object. Keys for nested + * properties are concatenated using a dot (`.`) as a separator, while array elements + * include their index in square brackets (`[]`). + * + * @param {Record} obj - The input object to be flattened. + * @param {string} [prefix=''] - The current prefix for the keys, used for recursion. + * @param {Record} [result={}] - The accumulator object that stores the flattened result. + * @returns {Record} A new object where all nested properties are flattened into + * a single level with their paths as keys. + * + * @example + * const input = { + * name: { + * name: 'Name', + * value: 'load-generator', + * status: null, + * explain: '...', + * }, + * }; + * + * const output = flattenObjectKeys(input); + * Output: + * { + * 'name.name': 'Name', + * 'name.value': 'load-generator', + * 'name.status': null, + * 'name.explain': '...', + * } + */ + +export const flattenObjectKeys = (obj: Record, prefix: string = '', result: Record = {}) => { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + const newKey = prefix ? `${prefix}.${key}` : key; + + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + // Recurse for nested objects + flattenObjectKeys(value, newKey, result); + } else if (Array.isArray(value)) { + value.forEach((item, index) => { + const arrayKey = `${newKey}[${index}]`; + + if (item !== null && typeof item === 'object') { + // Recurse for objects in arrays + flattenObjectKeys(item, arrayKey, result); + } else { + // Assign primitive array values + result[arrayKey] = item; + } + }); + } else { + // Assign non-object, non-array values + result[newKey] = value; + } + } + } + + return result; +}; diff --git a/frontend/webapp/utils/functions/formatters/index.ts b/frontend/webapp/utils/functions/formatters/index.ts index 60092f157..64249733c 100644 --- a/frontend/webapp/utils/functions/formatters/index.ts +++ b/frontend/webapp/utils/functions/formatters/index.ts @@ -1,8 +1,10 @@ export * from './clean-object-empty-strings-values'; export * from './extract-monitors'; +export * from './flatten-object-keys'; export * from './format-bytes'; export * from './get-id-from-sse-target'; export * from './get-sse-target-from-id'; export * from './parse-json-string-to-pretty-string'; export * from './safe-json-parse'; +export * from './safe-json-stringify'; export * from './stringify-non-string-values'; diff --git a/frontend/webapp/utils/functions/formatters/safe-json-stringify/index.ts b/frontend/webapp/utils/functions/formatters/safe-json-stringify/index.ts new file mode 100644 index 000000000..0351a873d --- /dev/null +++ b/frontend/webapp/utils/functions/formatters/safe-json-stringify/index.ts @@ -0,0 +1,3 @@ +export const safeJsonStringify = (obj?: Record, indent = 2) => { + return JSON.stringify(obj || {}, null, indent); +}; diff --git a/frontend/webapp/yarn.lock b/frontend/webapp/yarn.lock index 200d61fe5..40efdcef6 100644 --- a/frontend/webapp/yarn.lock +++ b/frontend/webapp/yarn.lock @@ -2265,6 +2265,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== +"@types/prismjs@^1.26.0": + version "1.26.5" + resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.5.tgz#72499abbb4c4ec9982446509d2f14fb8483869d6" + integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ== + "@types/prop-types@*": version "15.7.13" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" @@ -2953,6 +2958,11 @@ client-only@0.0.1: resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +clsx@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -5142,6 +5152,14 @@ pretty-bytes@^5.6.0: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== +prism-react-renderer@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz#ac63b7f78e56c8f2b5e76e823a976d5ede77e35f" + integrity sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig== + dependencies: + "@types/prismjs" "^1.26.0" + clsx "^2.0.0" + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"