k8s add hpa and ingresses UI (#2640)
* k8s add hpa and ingresses UI * add todo
This commit is contained in:
@@ -30,7 +30,7 @@ describe('ConfigMaps', () => {
|
||||
|
||||
// title
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('ConfigMap')).toBeInTheDocument();
|
||||
expect(getByText('Config Map')).toBeInTheDocument();
|
||||
|
||||
// values
|
||||
expect(getByText('Immutable')).toBeInTheDocument();
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ConfigMaps = ({ configMaps }: ConfigMapsProps) => {
|
||||
<Grid item key={i}>
|
||||
<InfoCard
|
||||
title={cm.metadata?.name ?? 'un-named service'}
|
||||
subheader="ConfigMap"
|
||||
subheader="Config Map"
|
||||
>
|
||||
<div>
|
||||
<StructuredMetadataTable
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as hpaFixture from './__fixtures__/horizontalpodautoscalers.json';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { HorizontalPodAutoscalers } from './HorizontalPodAutoscalers';
|
||||
|
||||
describe('HorizontalPodAutoscalers', () => {
|
||||
it('should render horizontalpodautoscaler', async () => {
|
||||
const { getByText, getAllByText } = render(
|
||||
wrapInTestApp(
|
||||
<HorizontalPodAutoscalers hpas={(hpaFixture as any).default} />,
|
||||
),
|
||||
);
|
||||
|
||||
// title
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('Horizontal Pod Autoscaler')).toBeInTheDocument();
|
||||
|
||||
expect(getByText('Scaling Target')).toBeInTheDocument();
|
||||
expect(getByText('Api Version: apps/v1')).toBeInTheDocument();
|
||||
expect(getByText('Kind: Deployment')).toBeInTheDocument();
|
||||
expect(getByText('Name: dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('Min Replicas')).toBeInTheDocument();
|
||||
expect(getByText('Max Replicas')).toBeInTheDocument();
|
||||
expect(getByText('15')).toBeInTheDocument();
|
||||
expect(getByText('Current Replicas')).toBeInTheDocument();
|
||||
expect(getByText('Desired Replicas')).toBeInTheDocument();
|
||||
expect(getByText('0')).toBeInTheDocument();
|
||||
expect(getByText('Target CPU Utilization Percentage')).toBeInTheDocument();
|
||||
expect(getByText('50')).toBeInTheDocument();
|
||||
expect(getByText('Current CPU Utilization Percentage')).toBeInTheDocument();
|
||||
expect(getByText('Last Scale Time')).toBeInTheDocument();
|
||||
expect(getAllByText('unknown')).toHaveLength(2);
|
||||
expect(getAllByText('10')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node';
|
||||
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
|
||||
import { orUnknown } from '../../utils';
|
||||
|
||||
type HorizontalPodAutoscalersProps = {
|
||||
hpas: V1HorizontalPodAutoscaler[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const HorizontalPodAutoscalers = ({
|
||||
hpas,
|
||||
}: HorizontalPodAutoscalersProps) => {
|
||||
return (
|
||||
<Grid container>
|
||||
{hpas.map((hpa, i) => {
|
||||
return (
|
||||
<Grid item key={i}>
|
||||
<InfoCard
|
||||
title={hpa.metadata?.name ?? 'un-named service'}
|
||||
subheader="Horizontal Pod Autoscaler"
|
||||
>
|
||||
<div>
|
||||
<StructuredMetadataTable
|
||||
metadata={{
|
||||
scalingTarget: orUnknown(hpa.spec?.scaleTargetRef),
|
||||
minReplicas: orUnknown(hpa.spec?.minReplicas),
|
||||
maxReplicas: orUnknown(hpa.spec?.maxReplicas),
|
||||
currentReplicas: orUnknown(hpa.status?.currentReplicas),
|
||||
desiredReplicas: orUnknown(hpa.status?.desiredReplicas),
|
||||
targetCPUUtilizationPercentage: orUnknown(
|
||||
hpa.spec?.targetCPUUtilizationPercentage,
|
||||
),
|
||||
currentCPUUtilizationPercentage: orUnknown(
|
||||
hpa.status?.currentCPUUtilizationPercentage,
|
||||
),
|
||||
lastScaleTime: orUnknown(hpa.status?.lastScaleTime),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
[
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"True\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"SucceededGetScale\",\"message\":\"the HPA controller was able to get the target's current scale\"},{\"type\":\"ScalingActive\",\"status\":\"False\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"FailedGetResourceMetric\",\"message\":\"the HPA was unable to compute the replica count: unable to get metrics for resource cpu: unable to fetch metrics from resource metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)\"}]",
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":15,\"minReplicas\":10,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"dice-roller\"},\"targetCPUUtilizationPercentage\":50}}\n"
|
||||
},
|
||||
"creationTimestamp": "2020-09-28T13:28:00.000Z",
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "autoscaling/v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
"f:autoscaling.alpha.kubernetes.io/conditions": {}
|
||||
}
|
||||
},
|
||||
"f:status": {
|
||||
"f:currentReplicas": {}
|
||||
}
|
||||
},
|
||||
"manager": "kube-controller-manager",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-28T13:28:15.000Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "autoscaling/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:maxReplicas": {},
|
||||
"f:minReplicas": {},
|
||||
"f:scaleTargetRef": {
|
||||
"f:apiVersion": {},
|
||||
"f:kind": {},
|
||||
"f:name": {}
|
||||
},
|
||||
"f:targetCPUUtilizationPercentage": {}
|
||||
}
|
||||
},
|
||||
"manager": "kubectl",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-28T13:28:21.000Z"
|
||||
}
|
||||
],
|
||||
"name": "dice-roller",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "698957",
|
||||
"selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/dice-roller",
|
||||
"uid": "a70c8a90-5605-4d7d-adea-05cfb8d9d446"
|
||||
},
|
||||
"spec": {
|
||||
"maxReplicas": 15,
|
||||
"minReplicas": 10,
|
||||
"scaleTargetRef": {
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"name": "dice-roller"
|
||||
},
|
||||
"targetCPUUtilizationPercentage": 50
|
||||
},
|
||||
"status": {
|
||||
"currentReplicas": 10,
|
||||
"desiredReplicas": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { HorizontalPodAutoscalers } from './HorizontalPodAutoscalers';
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as ingressesFixture from './__fixtures__/ingress.json';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { Ingresses } from './Ingresses';
|
||||
|
||||
describe('Ingresses', () => {
|
||||
it('should render ingress', async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<Ingresses ingresses={(ingressesFixture as any).default} />,
|
||||
),
|
||||
);
|
||||
|
||||
// title
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('Ingress')).toBeInTheDocument();
|
||||
|
||||
// values
|
||||
expect(getByText('Backend')).toBeInTheDocument();
|
||||
expect(getByText('Ip: 192.168.64.2')).toBeInTheDocument();
|
||||
expect(getByText('Rules')).toBeInTheDocument();
|
||||
expect(getByText('Host: nginx')).toBeInTheDocument();
|
||||
expect(getByText('Http:')).toBeInTheDocument();
|
||||
expect(getByText('Paths:')).toBeInTheDocument();
|
||||
expect(getByText('Service Name: dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('Service Port: 80')).toBeInTheDocument();
|
||||
expect(getByText('Path: /')).toBeInTheDocument();
|
||||
expect(getByText('Path Type: ImplementationSpecific')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node';
|
||||
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
|
||||
|
||||
type IngressesProps = {
|
||||
ingresses: ExtensionsV1beta1Ingress[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Ingresses = ({ ingresses }: IngressesProps) => {
|
||||
return (
|
||||
<Grid container>
|
||||
{ingresses.map((ingress, i) => {
|
||||
return (
|
||||
<Grid item key={i}>
|
||||
<InfoCard
|
||||
title={ingress.metadata?.name ?? 'un-named ingress'}
|
||||
subheader="Ingress"
|
||||
>
|
||||
<div>
|
||||
<StructuredMetadataTable
|
||||
metadata={{
|
||||
backend: ingress.status?.loadBalancer?.ingress,
|
||||
rules: ingress.spec?.rules,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
[
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"networking.k8s.io/v1beta1\",\"kind\":\"Ingress\",\"metadata\":{\"annotations\":{\"nginx.ingress.kubernetes.io/rewrite-target\":\"/$1\"},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"rules\":[{\"host\":\"nginx\",\"http\":{\"paths\":[{\"backend\":{\"serviceName\":\"dice-roller\",\"servicePort\":80},\"path\":\"/\"}]}}]}}\n",
|
||||
"nginx.ingress.kubernetes.io/rewrite-target": "/$1"
|
||||
},
|
||||
"creationTimestamp": "2020-09-28T13:28:00.000Z",
|
||||
"generation": 1,
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "networking.k8s.io/v1beta1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
".": {},
|
||||
"f:kubectl.kubernetes.io/last-applied-configuration": {},
|
||||
"f:nginx.ingress.kubernetes.io/rewrite-target": {}
|
||||
},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:backstage.io/kubernetes-id": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
"f:rules": {}
|
||||
}
|
||||
},
|
||||
"manager": "kubectl",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-28T13:28:21.000Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "networking.k8s.io/v1beta1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:status": {
|
||||
"f:loadBalancer": {
|
||||
"f:ingress": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"manager": "nginx-ingress-controller",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-28T13:28:40.000Z"
|
||||
}
|
||||
],
|
||||
"name": "dice-roller",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "699017",
|
||||
"selfLink": "/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/dice-roller",
|
||||
"uid": "e96994c0-49b9-4c1c-8ce0-72c5336fe960"
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"host": "nginx",
|
||||
"http": {
|
||||
"paths": [
|
||||
{
|
||||
"backend": {
|
||||
"serviceName": "dice-roller",
|
||||
"servicePort": 80
|
||||
},
|
||||
"path": "/",
|
||||
"pathType": "ImplementationSpecific"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"loadBalancer": {
|
||||
"ingress": [
|
||||
{
|
||||
"ip": "192.168.64.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { Ingresses } from './Ingresses';
|
||||
@@ -34,15 +34,25 @@ import {
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
import { DeploymentTables } from '../DeploymentTables';
|
||||
import { DeploymentTriple } from '../../types/types';
|
||||
import { V1ConfigMap, V1Service } from '@kubernetes/client-node';
|
||||
import {
|
||||
ExtensionsV1beta1Ingress,
|
||||
V1ConfigMap,
|
||||
V1HorizontalPodAutoscaler,
|
||||
V1Service,
|
||||
} from '@kubernetes/client-node';
|
||||
import { Services } from '../Services';
|
||||
import { ConfigMaps } from '../ConfigMaps';
|
||||
import { Ingresses } from '../Ingresses';
|
||||
import { HorizontalPodAutoscalers } from '../HorizontalPodAutoscalers';
|
||||
|
||||
interface GroupedResponses extends DeploymentTriple {
|
||||
services: V1Service[];
|
||||
configMaps: V1ConfigMap[];
|
||||
horizontalPodAutoscalers: V1HorizontalPodAutoscaler[];
|
||||
ingresses: ExtensionsV1beta1Ingress[];
|
||||
}
|
||||
|
||||
// TODO this could probably be a lodash groupBy
|
||||
const groupResponses = (fetchResponse: FetchResponse[]) => {
|
||||
return fetchResponse.reduce(
|
||||
(prev, next) => {
|
||||
@@ -62,6 +72,12 @@ const groupResponses = (fetchResponse: FetchResponse[]) => {
|
||||
case 'configmaps':
|
||||
prev.configMaps.push(...next.resources);
|
||||
break;
|
||||
case 'horizontalpodautoscalers':
|
||||
prev.horizontalPodAutoscalers.push(...next.resources);
|
||||
break;
|
||||
case 'ingresses':
|
||||
prev.ingresses.push(...next.resources);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return prev;
|
||||
@@ -72,6 +88,8 @@ const groupResponses = (fetchResponse: FetchResponse[]) => {
|
||||
deployments: [],
|
||||
services: [],
|
||||
configMaps: [],
|
||||
horizontalPodAutoscalers: [],
|
||||
ingresses: [],
|
||||
} as GroupedResponses,
|
||||
);
|
||||
};
|
||||
@@ -130,6 +148,8 @@ const Cluster = ({ clusterObjects }: ClusterProps) => {
|
||||
const groupedResponses = groupResponses(clusterObjects.resources);
|
||||
|
||||
const configMaps = groupedResponses.configMaps;
|
||||
const hpas = groupedResponses.horizontalPodAutoscalers;
|
||||
const ingresses = groupedResponses.ingresses;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -151,10 +171,20 @@ const Cluster = ({ clusterObjects }: ClusterProps) => {
|
||||
<Services services={groupedResponses.services} />
|
||||
</CardTab>
|
||||
{configMaps && (
|
||||
<CardTab value="three" label="ConfigMaps">
|
||||
<CardTab value="three" label="Config Maps">
|
||||
<ConfigMaps configMaps={configMaps} />
|
||||
</CardTab>
|
||||
)}
|
||||
{hpas && (
|
||||
<CardTab value="four" label="Horizontal Pod Autoscalers">
|
||||
<HorizontalPodAutoscalers hpas={hpas} />
|
||||
</CardTab>
|
||||
)}
|
||||
{ingresses && (
|
||||
<CardTab value="five" label="Ingresses">
|
||||
<Ingresses ingresses={ingresses} />
|
||||
</CardTab>
|
||||
)}
|
||||
</TabbedCard>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Display the value unknown rather than 'undefined'
|
||||
export function orUnknown<T>(val: T | undefined): T | string {
|
||||
return val ?? 'unknown';
|
||||
}
|
||||
Reference in New Issue
Block a user