From 9c9f4ff99027a022b626ec1cb5960d2f621dcded Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Fri, 4 Oct 2024 15:10:32 +0200 Subject: [PATCH 01/14] feat: render nested metadata as yaml Signed-off-by: Chris Langhout --- .changeset/eight-cougars-collect.md | 5 + .../StructuredMetadataTable.tsx | 110 +----- plugins/kubernetes/dev/index.tsx | 14 + .../src/__fixtures__/3-ingresses.json | 372 ++++++++++++++++++ 4 files changed, 412 insertions(+), 89 deletions(-) create mode 100644 .changeset/eight-cougars-collect.md create mode 100644 plugins/kubernetes/src/__fixtures__/3-ingresses.json diff --git a/.changeset/eight-cougars-collect.md b/.changeset/eight-cougars-collect.md new file mode 100644 index 0000000000..07b3a52ca1 --- /dev/null +++ b/.changeset/eight-cougars-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +better rendering of nested data; rendered as yaml now diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx index 3099edf9d8..657f656949 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx @@ -14,109 +14,41 @@ * limitations under the License. */ -import React, { Fragment, ReactElement } from 'react'; -import { - withStyles, - createStyles, - WithStyles, - Theme, -} from '@material-ui/core/styles'; +import React, { Fragment } from 'react'; import startCase from 'lodash/startCase'; import Typography from '@material-ui/core/Typography'; import { MetadataTable, MetadataTableItem, - MetadataList, - MetadataListItem, } from './MetadataTable'; +import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; +import { CodeSnippet } from '../CodeSnippet'; +import jsyaml from 'js-yaml'; export type StructuredMetadataTableListClassKey = 'root'; -const listStyle = createStyles({ - root: { - margin: '0 0', - listStyleType: 'none', - }, -}); - export type StructuredMetadataTableNestedListClassKey = 'root'; -const nestedListStyle = (theme: Theme) => - createStyles({ - root: { - ...listStyle.root, - paddingLeft: theme.spacing(1), - }, - }); - -interface StyleProps extends WithStyles { - children?: React.ReactNode; -} -// Sub Components -const StyledList = withStyles(listStyle, { - name: 'BackstageStructuredMetadataTableList', -})(({ classes, children }: StyleProps) => ( - {children} -)); -const StyledNestedList = withStyles(nestedListStyle, { - name: 'BackstageStructuredMetadataTableNestedList', -})(({ classes, children }: StyleProps) => ( - {children} -)); - -function renderList(list: Array, options: Options, nested: boolean) { - const values = list.map((item: any, index: number) => ( - - {toValue(item, options, nested)} - - )); - return nested ? ( - {values} - ) : ( - {values} - ); -} - -function renderMap( - map: { [key: string]: any }, - options: Options, - nested: boolean, -) { - const values = Object.keys(map).map(key => { - const value = toValue(map[key], options, true); - return ( - - - {`${options.titleFormat(key)}: `} - - {value} - - ); - }); - - return nested ? ( - {values} - ) : ( - {values} - ); -} - function toValue( - value: ReactElement | object | Array | boolean, - options: Options, - nested: boolean, + value: object | Array | boolean | string, ) { if (React.isValidElement(value)) { return {value}; } - if (value !== null && typeof value === 'object' && !Array.isArray(value)) { - return renderMap(value, options, nested); - } - - if (Array.isArray(value)) { - return renderList(value, options, nested); + if (value !== null && typeof value === 'object') { + return } if (typeof value === 'boolean') { @@ -129,8 +61,8 @@ function toValue( ); } -const ItemValue = ({ value, options }: { value: any; options: Options }) => ( - {toValue(value, options, false)} +const ItemValue = ({ value }: { value: any }) => ( + {toValue(value)} ); const TableItem = ({ @@ -139,12 +71,12 @@ const TableItem = ({ options, }: { title: string; - value: any; + value: JsonObject | JsonArray | JsonValue; options: Options; }) => { return ( - + ); }; diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index 2de5a17f25..008d6ce54b 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -34,6 +34,7 @@ import fixture2 from '../src/__fixtures__/2-deployments.json'; import fixture3 from '../src/__fixtures__/1-cronjobs.json'; import fixture4 from '../src/__fixtures__/2-cronjobs.json'; import fixture5 from '../src/__fixtures__/1-rollouts.json'; +import fixture6 from '../src/__fixtures__/3-ingresses.json'; import { TestApiProvider } from '@backstage/test-utils'; const mockEntity: Entity = { @@ -200,5 +201,18 @@ createDevApp() ), }) + .addPage({ + path: '/fixture-6', + title: 'Fixture 6', + element: ( + + + + + + ), + }) .registerPlugin(kubernetesPlugin) .render(); diff --git a/plugins/kubernetes/src/__fixtures__/3-ingresses.json b/plugins/kubernetes/src/__fixtures__/3-ingresses.json new file mode 100644 index 0000000000..94359c8239 --- /dev/null +++ b/plugins/kubernetes/src/__fixtures__/3-ingresses.json @@ -0,0 +1,372 @@ +{ + "ingresses": [ + { + "metadata": { + "name": "app-service-endpoint.dice-roller.backstage.io", + "namespace": "dice-roller", + "uid": "00000000-0000-0000-0000-000000000000", + "resourceVersion": "4749260703", + "generation": 1, + "creationTimestamp": "2023-08-16T14:52:16Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "ingress-class": "istio-internal" + }, + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "spec", + "metacontroller.k8s.io/last-applied-configuration": "spec", + "nginx.ingress.kubernetes.io/auth-signin": "https://backstage.io/authenticate", + "nginx.ingress.kubernetes.io/configuration-snippet": "config", + "nginx.ingress.kubernetes.io/service-upstream": "true", + "nginx.ingress.kubernetes.io/upstream-vhost": "app-service-endpoint.dice-roller.backstage.services" + }, + "ownerReferences": [ + { + "apiVersion": "networking.backstage.com/v1beta1", + "kind": "MeshIntegration", + "name": "app-service-endpoint", + "uid": "00000000-0000-0000-0000-000000000000", + "controller": true, + "blockOwnerDeletion": true + } + ], + "managedFields": [ + { + "manager": "nginx-ingress-controller", + "operation": "Update", + "apiVersion": "networking.k8s.io/v1", + "time": "2023-08-16T14:52:56Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:loadBalancer": { + "f:ingress": {} + } + } + }, + "subresource": "status" + }, + { + "manager": "kubectl-client-side-apply", + "operation": "Update", + "apiVersion": "networking.k8s.io/v1", + "time": "2023-12-15T16:33:17Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {}, + "f:nginx.ingress.kubernetes.io/auth-signin": {}, + "f:nginx.ingress.kubernetes.io/configuration-snippet": {}, + "f:nginx.ingress.kubernetes.io/service-upstream": {}, + "f:nginx.ingress.kubernetes.io/upstream-vhost": {} + }, + "f:labels": { + ".": {}, + "f:backstage.io/kubernetes-id": {}, + "f:ingress-class": {} + } + }, + "f:spec": { + "f:ingressClassName": {}, + "f:rules": {}, + "f:tls": {} + } + } + }, + { + "manager": "kubectl-label", + "operation": "Update", + "apiVersion": "networking.k8s.io/v1", + "time": "2024-02-21T15:00:44Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:controller-uid": {} + } + } + } + }, + { + "manager": "metacontroller", + "operation": "Update", + "apiVersion": "networking.k8s.io/v1", + "time": "2024-02-21T15:01:01Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:metacontroller.k8s.io/last-applied-configuration": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"00000000-0000-0000-0000-000000000000\"}": {} + } + } + } + } + ] + }, + "spec": { + "ingressClassName": "istio-internal", + "tls": { + "hosts": ["app-service-endpoint.dice-roller.backstage.io"], + "secretName": "dice-roller-wildcard-certificate" + }, + "rules": [ + { + "host": "app-service-endpoint.dice-roller.backstage.io", + "http": { + "paths": [ + { + "pathType": "ImplementationSpecific", + "backend": { + "service": { + "name": "app-service-endpoint", + "port": { + "number": 8080 + } + } + } + }, + { + "pathType": "Second item in the list", + "backend": { + "service": { + "name": "app-service-endpoint", + "port": { + "number": 8080 + } + } + } + } + ] + } + } + ] + }, + "status": { + "loadBalancer": { + "ingress": [ + { + "ip": "1.2.3.4" + } + ] + } + } + } + ], + "deployments": [ + { + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "2", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"replicas\":10,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.14.2\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]}]}}}}\n" + }, + "creationTimestamp": "2020-09-23T12:00:55.000Z", + "generation": 3, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "managedFields": [ + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "f:spec": { + "f:progressDeadlineSeconds": {}, + "f:replicas": {}, + "f:revisionHistoryLimit": {}, + "f:selector": { + "f:matchLabels": { + ".": {}, + "f:app": {} + } + }, + "f:strategy": { + "f:rollingUpdate": { + ".": {}, + "f:maxSurge": {}, + "f:maxUnavailable": {} + }, + "f:type": {} + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + } + } + }, + "manager": "kubectl", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:deployment.kubernetes.io/revision": {} + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:conditions": { + ".": {}, + "k:{\"type\":\"Available\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Progressing\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {}, + "f:updatedReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "593230", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/dice-roller", + "uid": "7551e949-42d1-4061-83c5-9da107186e47" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 10, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "dice-roller" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller" + } + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 10, + "conditions": [ + { + "lastTransitionTime": "2020-09-23T12:00:55.000Z", + "lastUpdateTime": "2020-09-24T11:39:28.000Z", + "message": "ReplicaSet \"dice-roller-6c8646bfd\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + }, + { + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "lastUpdateTime": "2020-09-25T09:58:55.000Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + } + ], + "observedGeneration": 3, + "readyReplicas": 10, + "replicas": 10, + "updatedReplicas": 10 + } + } + ] +} From 0d468fcd7852770bee7d6779ddff060f63b2108e Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Fri, 4 Oct 2024 15:31:37 +0200 Subject: [PATCH 02/14] fix: lint errors Signed-off-by: Chris Langhout --- packages/core-components/package.json | 1 + .../StructuredMetadataTable.tsx | 35 ++++++++----------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 13d3785c4d..add79c01f5 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -73,6 +73,7 @@ "d3-shape": "^3.0.0", "d3-zoom": "^3.0.0", "dagre": "^0.8.5", + "js-yaml": "^4.1.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", "lodash": "^4.17.21", diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx index 657f656949..55c8895dca 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx @@ -18,11 +18,7 @@ import React, { Fragment } from 'react'; import startCase from 'lodash/startCase'; import Typography from '@material-ui/core/Typography'; -import { - MetadataTable, - MetadataTableItem, -} from './MetadataTable'; -import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; +import { MetadataTable, MetadataTableItem } from './MetadataTable'; import { CodeSnippet } from '../CodeSnippet'; import jsyaml from 'js-yaml'; @@ -30,25 +26,24 @@ export type StructuredMetadataTableListClassKey = 'root'; export type StructuredMetadataTableNestedListClassKey = 'root'; -function toValue( - value: object | Array | boolean | string, -) { +function toValue(value: object | Array | boolean | string) { if (React.isValidElement(value)) { return {value}; } if (value !== null && typeof value === 'object') { - return + return ( + + ); } if (typeof value === 'boolean') { @@ -71,7 +66,7 @@ const TableItem = ({ options, }: { title: string; - value: JsonObject | JsonArray | JsonValue; + value: any; options: Options; }) => { return ( From 28bb3749d8e09a43fec70a31c0d1407b15457e40 Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Tue, 8 Oct 2024 11:17:21 +0200 Subject: [PATCH 03/14] chore: adapt tests; keys are displayed as is instead of formatted and capitalized Signed-off-by: Chris Langhout --- .../components/IngressesAccordions/IngressDrawer.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/kubernetes-react/src/components/IngressesAccordions/IngressDrawer.test.tsx b/plugins/kubernetes-react/src/components/IngressesAccordions/IngressDrawer.test.tsx index cfacb24ee4..e504aefbb9 100644 --- a/plugins/kubernetes-react/src/components/IngressesAccordions/IngressDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/IngressesAccordions/IngressDrawer.test.tsx @@ -37,13 +37,13 @@ describe('IngressDrawer', () => { expect(screen.getByText('YAML')).toBeInTheDocument(); expect(screen.getByText('Rules')).toBeInTheDocument(); expect( - screen.getByText(textContentMatcher('Host: api.awesome-host.io')), + screen.getByText(textContentMatcher('host: api.awesome-host.io')), ).toBeInTheDocument(); expect( - screen.getAllByText(textContentMatcher('Service Port: 80')), + screen.getAllByText(textContentMatcher('servicePort: 80')), ).toHaveLength(2); expect( - screen.getAllByText(textContentMatcher('Service Name: awesome-service')), + screen.getAllByText(textContentMatcher('serviceName: awesome-service')), ).toHaveLength(2); }); }); From cd1256e5a4aa2605e95e6f074c58afcd3ce00c30 Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Tue, 8 Oct 2024 11:39:34 +0200 Subject: [PATCH 04/14] chore: change more tests to match yaml display Signed-off-by: Chris Langhout --- .../DeploymentDrawer.test.tsx | 8 ++++---- .../ServicesAccordions/ServiceDrawer.test.tsx | 4 ++-- .../StatefulSetDrawer.test.tsx | 12 ++++++------ plugins/kubernetes/dev/index.tsx | 14 ++++++++++++++ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/plugins/kubernetes-react/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx b/plugins/kubernetes-react/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx index 1e8515f81a..002c4c8ff4 100644 --- a/plugins/kubernetes-react/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx @@ -40,13 +40,13 @@ describe('DeploymentDrawer', () => { expect(getAllByText('Deployment')).toHaveLength(2); expect(getByText('YAML')).toBeInTheDocument(); expect(getByText('Strategy')).toBeInTheDocument(); - expect(getByText('Rolling Update:')).toBeInTheDocument(); - expect(getByText(textContentMatcher('Max Surge: 25%'))).toBeInTheDocument(); + expect(getByText('rollingUpdate:')).toBeInTheDocument(); + expect(getByText(textContentMatcher('maxSurge: 25%'))).toBeInTheDocument(); expect( - getByText(textContentMatcher('Max Unavailable: 25%')), + getByText(textContentMatcher('maxUnavailable: 25%')), ).toBeInTheDocument(); expect( - getByText(textContentMatcher('Type: RollingUpdate')), + getByText(textContentMatcher('type: RollingUpdate')), ).toBeInTheDocument(); expect(getByText('Min Ready Seconds')).toBeInTheDocument(); expect(getByText('???')).toBeInTheDocument(); diff --git a/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.test.tsx b/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.test.tsx index 7eec2926c4..692c0a7c8c 100644 --- a/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.test.tsx @@ -39,10 +39,10 @@ describe('ServiceDrawer', () => { expect(screen.getByText('Cluster IP')).toBeInTheDocument(); expect(screen.getByText('Ports')).toBeInTheDocument(); expect( - screen.getByText(textContentMatcher('Target Port: 1997')), + screen.getByText(textContentMatcher('targetPort: 1997')), ).toBeInTheDocument(); expect( - screen.getByText(textContentMatcher('App: awesome-service')), + screen.getByText(textContentMatcher('app: awesome-service')), ).toBeInTheDocument(); }); }); diff --git a/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx b/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx index d69ab15f1a..3e4ff56a35 100644 --- a/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx @@ -40,20 +40,20 @@ describe('StatefulSetDrawer', () => { expect(getByText('StatefulSet')).toBeInTheDocument(); expect(getByText('YAML')).toBeInTheDocument(); expect( - getByText(textContentMatcher('Type: RollingUpdate')), + getByText(textContentMatcher('type: RollingUpdate')), ).toBeInTheDocument(); - expect(getByText('Rolling Update:')).toBeInTheDocument(); - expect(getByText(textContentMatcher('Max Surge: 25%'))).toBeInTheDocument(); + expect(getByText('rollingUpdate:')).toBeInTheDocument(); + expect(getByText(textContentMatcher('maxSurge: 25%'))).toBeInTheDocument(); expect( - getByText(textContentMatcher('Max Unavailable: 25%')), + getByText(textContentMatcher('maxUnavailable: 25%')), ).toBeInTheDocument(); expect(getByText('Pod Management Policy')).toBeInTheDocument(); expect(getByText('Parallel')).toBeInTheDocument(); expect(getByText('Service Name')).toBeInTheDocument(); expect(getByText('Selector')).toBeInTheDocument(); - expect(getByText('Match Labels:')).toBeInTheDocument(); + expect(getByText('matchLabels:')).toBeInTheDocument(); expect( - getByText(textContentMatcher('App: dice-roller')), + getByText(textContentMatcher('app: dice-roller')), ).toBeInTheDocument(); expect(getByText('Revision History Limit')).toBeInTheDocument(); expect(getByText('10')).toBeInTheDocument(); diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index 008d6ce54b..a33d3beee9 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -35,6 +35,7 @@ import fixture3 from '../src/__fixtures__/1-cronjobs.json'; import fixture4 from '../src/__fixtures__/2-cronjobs.json'; import fixture5 from '../src/__fixtures__/1-rollouts.json'; import fixture6 from '../src/__fixtures__/3-ingresses.json'; +import fixture7 from '../src/__fixtures__/2-statefulsets.json'; import { TestApiProvider } from '@backstage/test-utils'; const mockEntity: Entity = { @@ -214,5 +215,18 @@ createDevApp() ), }) + .addPage({ + path: '/fixture-7', + title: 'Fixture 7', + element: ( + + + + + + ), + }) .registerPlugin(kubernetesPlugin) .render(); From 4a58a408896d8ff84ed019bf1950dd045f356242 Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Tue, 8 Oct 2024 12:54:24 +0200 Subject: [PATCH 05/14] chore: change more tests to match yaml display Signed-off-by: Chris Langhout --- .../StructuredMetadataTable.test.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx index 4eb05e715e..25c5d0687c 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx @@ -66,7 +66,7 @@ describe('', () => { expect(getByText(startCase(value))).toBeInTheDocument(); }); metadata.arrayField.forEach(value => { - expect(getByText(value)).toBeInTheDocument(); + expect(getByText(new RegExp(value))).toBeInTheDocument(); }); }); @@ -127,9 +127,11 @@ describe('', () => { const rendered = render(); expect(rendered.queryByText(/^Test A/)).toBeInTheDocument(); expect(rendered.queryByText(/^Test B/)).toBeInTheDocument(); - expect(rendered.queryByText(/^Test C/)).toBeInTheDocument(); expect(rendered.queryByText(/^Test D/)).toBeInTheDocument(); - expect(rendered.queryByText(/^Test E/)).toBeInTheDocument(); + + // nested content is displayed as yaml, so not affected by formatting + expect(rendered.queryByText(/^testC/)).toBeInTheDocument(); + expect(rendered.queryByText(/^testE/)).toBeInTheDocument(); }); it('should be possible to disable it', async () => { @@ -166,9 +168,11 @@ describe('', () => { ); expect(rendered.queryByText(/^tEsTa/)).toBeInTheDocument(); expect(rendered.queryByText(/^tEsTb/)).toBeInTheDocument(); - expect(rendered.queryByText(/^tEsTc/)).toBeInTheDocument(); expect(rendered.queryByText(/^tEsTd/)).toBeInTheDocument(); - expect(rendered.queryByText(/^tEsTe/)).toBeInTheDocument(); + + // nested content is displayed as yaml, so not affected by formatting + expect(rendered.queryByText(/^testC/)).toBeInTheDocument(); + expect(rendered.queryByText(/^testE/)).toBeInTheDocument(); }); }); }); From 58e13832418cb5051fbde2cdff641b7d14a37b73 Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Tue, 8 Oct 2024 14:15:22 +0200 Subject: [PATCH 06/14] fix: changeset properly states what is changing Signed-off-by: Chris Langhout --- .changeset/eight-cougars-collect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eight-cougars-collect.md b/.changeset/eight-cougars-collect.md index 07b3a52ca1..3acb521bc4 100644 --- a/.changeset/eight-cougars-collect.md +++ b/.changeset/eight-cougars-collect.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -better rendering of nested data; rendered as yaml now +`StructuredMetadataTable` now renders data as yaml, improving readability. From b6c5ea2abac8feb6ad7ef0ade8c8dafb99d5be0e Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Sat, 4 Jan 2025 16:01:35 +0200 Subject: [PATCH 07/14] chore: change yaml rendering to be an option and enable for kubernetes plugin FE components Signed-off-by: Chris Langhout --- app-config.yaml | 2 + .../StructuredMetadataTable.tsx | 134 +++++++++++++++--- .../ClusterOverview/ClusterOverview.tsx | 1 + .../src/components/Nodes/Nodes.tsx | 7 +- .../CustomResources/DefaultCustomResource.tsx | 5 +- .../IngressesAccordions.tsx | 3 + ...ubernetesStructuredMetadataTableDrawer.tsx | 1 + .../Pods/PodDrawer/ContainerCard.tsx | 1 + .../ServicesAccordions/ServicesAccordions.tsx | 1 + 9 files changed, 131 insertions(+), 24 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 64c2fa5c4c..3abf08af0e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -322,6 +322,8 @@ catalog: # Backstage example templates - type: file target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml + - type: file + target: ../../plugins/scaffolder-backend/sample-templates/big-review-step.yaml rules: - allow: [Template] scaffolder: diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx index 55c8895dca..b839280cdd 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx @@ -14,50 +14,138 @@ * limitations under the License. */ -import React, { Fragment } from 'react'; +import React, { Fragment, ReactElement } from 'react'; import startCase from 'lodash/startCase'; import Typography from '@material-ui/core/Typography'; -import { MetadataTable, MetadataTableItem } from './MetadataTable'; +import { + MetadataList, + MetadataListItem, + MetadataTable, + MetadataTableItem, +} from './MetadataTable'; import { CodeSnippet } from '../CodeSnippet'; import jsyaml from 'js-yaml'; +import { + Theme, + createStyles, + WithStyles, + withStyles, +} from '@material-ui/core/styles'; export type StructuredMetadataTableListClassKey = 'root'; -export type StructuredMetadataTableNestedListClassKey = 'root'; +const listStyle = createStyles({ + root: { + margin: '0 0', + listStyleType: 'none', + }, +}); -function toValue(value: object | Array | boolean | string) { +export type StructuredMetadataTableNestedListClassKey = 'root'; +const nestedListStyle = (theme: Theme) => + createStyles({ + root: { + ...listStyle.root, + paddingLeft: theme.spacing(1), + }, + }); + +interface StyleProps extends WithStyles { + children?: React.ReactNode; +} +// Sub Components +const StyledList = withStyles(listStyle, { + name: 'BackstageStructuredMetadataTableList', +})(({ classes, children }: StyleProps) => ( + {children} +)); +const StyledNestedList = withStyles(nestedListStyle, { + name: 'BackstageStructuredMetadataTableNestedList', +})(({ classes, children }: StyleProps) => ( + {children} +)); + +function renderList(list: Array, options: Options, nested: boolean) { + const values = list.map((item: any, index: number) => ( + + {toValue(item, options, nested)} + + )); + return nested ? ( + {values} + ) : ( + {values} + ); +} + +function renderMap( + map: { [key: string]: any }, + options: Options, + nested: boolean, +) { + const values = Object.keys(map).map(key => { + const value = toValue(map[key], options, true); + return ( + + + {`${options.titleFormat(key)}: `} + + {value} + + ); + }); + + return nested ? ( + {values} + ) : ( + {values} + ); +} + +function toValue( + value: ReactElement | object | Array | boolean, + options: Options, + nested: boolean, +) { if (React.isValidElement(value)) { return {value}; } - if (value !== null && typeof value === 'object') { - return ( - - ); + if (options.nestedValuesAsYaml) { + return ( + + ); + } + if (!Array.isArray(value)) { + return renderMap(value, options, nested); + } + } + + if (Array.isArray(value)) { + return renderList(value, options, nested); } if (typeof value === 'boolean') { return {value ? '✅' : '❌'}; } - return ( {value} ); } -const ItemValue = ({ value }: { value: any }) => ( - {toValue(value)} +const ItemValue = ({ value, options }: { value: any; options: Options }) => ( + {toValue(value, options, false)} ); const TableItem = ({ @@ -71,7 +159,7 @@ const TableItem = ({ }) => { return ( - + ); }; @@ -94,6 +182,7 @@ export interface StructuredMetadataTableProps { * @returns Formatted key */ titleFormat?: (key: string) => string; + nestedValuesAsYaml?: boolean; }; } @@ -101,9 +190,10 @@ type Options = Required>; /** @public */ export function StructuredMetadataTable(props: StructuredMetadataTableProps) { - const { metadata, dense = true, options = {} } = props; + const { metadata, dense = true, options } = props; const metadataItems = mapToItems(metadata, { titleFormat: startCase, + nestedValuesAsYaml: options?.nestedValuesAsYaml ?? false, ...options, }); return {metadataItems}; diff --git a/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx b/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx index b77b2dff88..af741239e5 100644 --- a/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx +++ b/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx @@ -58,6 +58,7 @@ export const ClusterOverview = () => { 'OIDC Token Provider': value.oidcTokenProvider ?? 'N/A', 'Dashboard Link': value.dashboardUrl ?? 'N/A', }} + options={{ nestedValuesAsYaml: true }} /> )} diff --git a/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx b/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx index 0b3f7ddb60..9307b11a6e 100644 --- a/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx +++ b/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx @@ -50,7 +50,10 @@ const defaultColumns: TableColumn[] = [ Node Info - + Addresses @@ -61,6 +64,7 @@ const defaultColumns: TableColumn[] = [ return accum; }, {} as any) ?? {} } + options={{ nestedValuesAsYaml: true }} /> @@ -72,6 +76,7 @@ const defaultColumns: TableColumn[] = [ return accum; }, {} as any) ?? {} } + options={{ nestedValuesAsYaml: true }} /> diff --git a/plugins/kubernetes-react/src/components/CustomResources/DefaultCustomResource.tsx b/plugins/kubernetes-react/src/components/CustomResources/DefaultCustomResource.tsx index ee484ecda8..d34ba23f7b 100644 --- a/plugins/kubernetes-react/src/components/CustomResources/DefaultCustomResource.tsx +++ b/plugins/kubernetes-react/src/components/CustomResources/DefaultCustomResource.tsx @@ -84,7 +84,10 @@ const DefaultCustomResourceAccordion = ({ {Object.prototype.hasOwnProperty.call(customResource, 'status') && ( - + )} diff --git a/plugins/kubernetes-react/src/components/IngressesAccordions/IngressesAccordions.tsx b/plugins/kubernetes-react/src/components/IngressesAccordions/IngressesAccordions.tsx index 5962e78f59..d2d674042b 100644 --- a/plugins/kubernetes-react/src/components/IngressesAccordions/IngressesAccordions.tsx +++ b/plugins/kubernetes-react/src/components/IngressesAccordions/IngressesAccordions.tsx @@ -65,6 +65,9 @@ const IngressCard = ({ ingress }: IngressCardProps) => { metadata={{ ...ingress.spec, }} + options={{ + nestedValuesAsYaml: true, + }} /> ); }; diff --git a/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx b/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx index b31d3f9aa7..0a5802ddc8 100644 --- a/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx +++ b/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx @@ -237,6 +237,7 @@ const KubernetesStructuredMetadataTableDrawerContent = < {!isYaml && ( )} diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx index 1919bc112d..6aed2c75d7 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx @@ -176,6 +176,7 @@ export const ContainerCard: React.FC = ({ containerSpec, containerStatus, )} + options={{ nestedValuesAsYaml: true }} /> {containerMetrics && ( diff --git a/plugins/kubernetes-react/src/components/ServicesAccordions/ServicesAccordions.tsx b/plugins/kubernetes-react/src/components/ServicesAccordions/ServicesAccordions.tsx index 71832fd35c..c44a1d158c 100644 --- a/plugins/kubernetes-react/src/components/ServicesAccordions/ServicesAccordions.tsx +++ b/plugins/kubernetes-react/src/components/ServicesAccordions/ServicesAccordions.tsx @@ -77,6 +77,7 @@ const ServiceCard = ({ service }: ServiceCardProps) => { ports: service.spec?.ports, ...metadata, }} + options={{ nestedValuesAsYaml: true }} /> ); }; From 9602eb06ce3dc234d4d34ef494bb8d92d6c21ffe Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Sat, 4 Jan 2025 17:33:29 +0200 Subject: [PATCH 08/14] fix: build api reports Signed-off-by: Chris Langhout --- packages/core-components/report.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index c3f7cf9a6a..c706eef2bb 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -1307,6 +1307,7 @@ export interface StructuredMetadataTableProps { // (undocumented) options?: { titleFormat?: (key: string) => string; + nestedValuesAsYaml?: boolean; }; } From b7d03476785fe1e01fe266e44c9985bc7ae3ae07 Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Sat, 4 Jan 2025 18:28:54 +0200 Subject: [PATCH 09/14] fix: tests Signed-off-by: Chris Langhout --- .../StructuredMetadataTable.test.tsx | 11 ++++++++--- plugins/kubernetes/dev/index.tsx | 11 +++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx index 25c5d0687c..8e6c4bbff0 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx @@ -124,14 +124,19 @@ describe('', () => { }; it('should make keys human readable', async () => { - const rendered = render(); + const rendered = render( + , + ); expect(rendered.queryByText(/^Test A/)).toBeInTheDocument(); expect(rendered.queryByText(/^Test B/)).toBeInTheDocument(); expect(rendered.queryByText(/^Test D/)).toBeInTheDocument(); // nested content is displayed as yaml, so not affected by formatting expect(rendered.queryByText(/^testC/)).toBeInTheDocument(); - expect(rendered.queryByText(/^testE/)).toBeInTheDocument(); + expect(rendered.queryByText(/testE: stuff/)).toBeInTheDocument(); }); it('should be possible to disable it', async () => { @@ -163,7 +168,7 @@ describe('', () => { const rendered = render( , ); expect(rendered.queryByText(/^tEsTa/)).toBeInTheDocument(); diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index a33d3beee9..91b97cbfc5 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -37,6 +37,7 @@ import fixture5 from '../src/__fixtures__/1-rollouts.json'; import fixture6 from '../src/__fixtures__/3-ingresses.json'; import fixture7 from '../src/__fixtures__/2-statefulsets.json'; import { TestApiProvider } from '@backstage/test-utils'; +import { StructuredMetadataTable } from '@backstage/core-components'; const mockEntity: Entity = { apiVersion: 'backstage.io/v1alpha1', @@ -136,6 +137,12 @@ class MockKubernetesClient implements KubernetesApi { } } +const metadata = { + testA: 'stuff', + testB: { testC: 'stuff' }, + testD: [{ testE: 'stuff' }], +}; + createDevApp() .addPage({ path: '/fixture-1', @@ -223,6 +230,10 @@ createDevApp() apis={[[kubernetesApiRef, new MockKubernetesClient(fixture7)]]} > + From eddcb72514136394b8f0e7bd289442b25294b51a Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Wed, 8 Jan 2025 16:07:03 +0200 Subject: [PATCH 10/14] fix: remove unneeded change Signed-off-by: Chris Langhout --- app-config.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 3abf08af0e..64c2fa5c4c 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -322,8 +322,6 @@ catalog: # Backstage example templates - type: file target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml - - type: file - target: ../../plugins/scaffolder-backend/sample-templates/big-review-step.yaml rules: - allow: [Template] scaffolder: From ebd3e85e1479d3316eacb4fa1dc98e8846b648a2 Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Thu, 9 Jan 2025 13:01:43 +0100 Subject: [PATCH 11/14] Update .changeset/eight-cougars-collect.md Co-authored-by: Vincenzo Scamporlino Signed-off-by: Chris Langhout --- .changeset/eight-cougars-collect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eight-cougars-collect.md b/.changeset/eight-cougars-collect.md index 3acb521bc4..edaa18d826 100644 --- a/.changeset/eight-cougars-collect.md +++ b/.changeset/eight-cougars-collect.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -`StructuredMetadataTable` now renders data as yaml, improving readability. +Added `nestedValuesAsYaml` option to `StructuredMetadataTable` to render data as yaml. From b3000dc294b6158ff0a71078a888bf1cd593d308 Mon Sep 17 00:00:00 2001 From: Chris Langhout Date: Thu, 9 Jan 2025 14:09:52 +0200 Subject: [PATCH 12/14] chore: add kubernetes packages in changeset Signed-off-by: Chris Langhout --- .changeset/eight-cougars-collect.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/eight-cougars-collect.md b/.changeset/eight-cougars-collect.md index edaa18d826..66ae59a27f 100644 --- a/.changeset/eight-cougars-collect.md +++ b/.changeset/eight-cougars-collect.md @@ -1,5 +1,7 @@ --- '@backstage/core-components': patch +'@backstage/plugin-kubernetes-cluster': patch +'@backstage/plugin-kubernetes-react': patch --- Added `nestedValuesAsYaml` option to `StructuredMetadataTable` to render data as yaml. From d80b08f066a3f67996218804c13fea51ce626559 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 16 Jan 2025 20:00:40 +0100 Subject: [PATCH 13/14] kubernetes: split changeset Signed-off-by: Vincenzo Scamporlino --- .changeset/eight-cougars-collect-due.md | 6 ++++++ .changeset/eight-cougars-collect.md | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changeset/eight-cougars-collect-due.md diff --git a/.changeset/eight-cougars-collect-due.md b/.changeset/eight-cougars-collect-due.md new file mode 100644 index 0000000000..ff997f876a --- /dev/null +++ b/.changeset/eight-cougars-collect-due.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-cluster': patch +'@backstage/plugin-kubernetes-react': patch +--- + +Improved rendering of Kubernetes resources' metadata. diff --git a/.changeset/eight-cougars-collect.md b/.changeset/eight-cougars-collect.md index 66ae59a27f..edaa18d826 100644 --- a/.changeset/eight-cougars-collect.md +++ b/.changeset/eight-cougars-collect.md @@ -1,7 +1,5 @@ --- '@backstage/core-components': patch -'@backstage/plugin-kubernetes-cluster': patch -'@backstage/plugin-kubernetes-react': patch --- Added `nestedValuesAsYaml` option to `StructuredMetadataTable` to render data as yaml. From 86c218a9a114ce7cbfab5ea5bdc7f473ccb86d90 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 24 Jan 2025 08:53:36 +0100 Subject: [PATCH 14/14] chore: fix yarn.lock Signed-off-by: blam --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 7cd8bb3cdc..e212f245b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4291,6 +4291,7 @@ __metadata: d3-zoom: ^3.0.0 dagre: ^0.8.5 history: ^5.0.0 + js-yaml: ^4.1.0 linkify-react: 4.1.3 linkifyjs: 4.1.3 lodash: ^4.17.21