Skip to content

Commit 8c55180

Browse files
committed
frontend: components: add seperate component to display env variables
1 parent b228929 commit 8c55180

22 files changed

+667
-12
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2025 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Meta, StoryFn } from '@storybook/react';
18+
import { TestContext } from '../../../test';
19+
import { EnvVarGrid } from './EnvVarGrid';
20+
21+
export default {
22+
title: 'Resource/EnvVarGrid',
23+
component: EnvVarGrid,
24+
argTypes: {
25+
namespace: { control: 'text' },
26+
cluster: { control: 'text' },
27+
},
28+
decorators: [
29+
Story => (
30+
<TestContext>
31+
<Story />
32+
</TestContext>
33+
),
34+
],
35+
} as Meta;
36+
37+
const Template: StoryFn<React.ComponentProps<typeof EnvVarGrid>> = args => <EnvVarGrid {...args} />;
38+
39+
export const Default = Template.bind({});
40+
Default.args = {
41+
namespace: 'default',
42+
cluster: 'minikube',
43+
envVars: [
44+
{ name: 'NODE_ENV', value: 'production' },
45+
{ name: 'DEBUG', value: 'true' },
46+
{ name: 'PLAINTEXT', value: '127.0.0.1' },
47+
],
48+
};
49+
50+
export const ComplexReferences = Template.bind({});
51+
ComplexReferences.args = {
52+
namespace: 'default',
53+
cluster: 'minikube',
54+
envVars: [
55+
{
56+
name: 'API_KEY',
57+
valueFrom: {
58+
secretKeyRef: { name: 'my-secret', key: 'api-key' },
59+
},
60+
},
61+
{
62+
name: 'APP_CONFIG',
63+
valueFrom: {
64+
configMapKeyRef: { name: 'app-config', key: 'config.json' },
65+
},
66+
},
67+
{
68+
name: 'MY_POD_IP',
69+
valueFrom: {
70+
fieldRef: { fieldPath: 'status.podIP', apiVersion: 'v1' },
71+
},
72+
},
73+
{
74+
name: 'CPU_LIMIT',
75+
valueFrom: {
76+
resourceFieldRef: { resource: 'limits.cpu' },
77+
},
78+
},
79+
],
80+
};
81+
82+
export const EdgeCases = Template.bind({});
83+
EdgeCases.args = {
84+
namespace: 'default',
85+
cluster: 'minikube',
86+
envVars: [
87+
{
88+
name: 'MISSING_SECRET_NAME',
89+
valueFrom: {
90+
// @ts-ignore - Testing resilience against missing data
91+
secretKeyRef: { key: 'some-key' },
92+
},
93+
},
94+
{
95+
name: 'MISSING_CONFIG_MAP',
96+
valueFrom: {
97+
// @ts-ignore - Testing resilience against missing data
98+
configMapKeyRef: { key: 'my-config' },
99+
},
100+
},
101+
{
102+
name: 'EMPTY_VALUE_FROM',
103+
valueFrom: {},
104+
},
105+
{
106+
name: 'MALFORMED_FIELD_REF',
107+
valueFrom: {
108+
fieldRef: {} as any,
109+
},
110+
},
111+
],
112+
};
113+
114+
export const ManyVariables = Template.bind({});
115+
ManyVariables.args = {
116+
namespace: 'default',
117+
cluster: 'minikube',
118+
envVars: Array.from({ length: 35 }, (_, i) => ({
119+
name: `VAR_${i}`,
120+
value: `value-${i}`,
121+
})),
122+
};
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* Copyright 2025 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Icon } from '@iconify/react';
18+
import Box from '@mui/material/Box';
19+
import Button from '@mui/material/Button';
20+
import Typography, { TypographyProps } from '@mui/material/Typography';
21+
import React from 'react';
22+
import { useTranslation } from 'react-i18next';
23+
import { KubeContainer } from '../../../lib/k8s/cluster';
24+
import Link from '../Link';
25+
26+
interface EnvVarGridProps {
27+
envVars: NonNullable<KubeContainer['env']>;
28+
namespace: string;
29+
cluster: string;
30+
}
31+
32+
export function EnvVarGrid(props: EnvVarGridProps) {
33+
const { envVars = [], namespace, cluster } = props;
34+
const { t } = useTranslation();
35+
const [expanded, setExpanded] = React.useState(false);
36+
const defaultNumShown = 20;
37+
38+
const validEnvVars = envVars.filter(
39+
env => !!env && typeof env.name === 'string' && env.name.trim() !== ''
40+
);
41+
42+
const EnvEntry = React.forwardRef((props: TypographyProps, ref: React.Ref<HTMLElement>) => {
43+
return (
44+
<Typography
45+
{...props}
46+
sx={theme => ({
47+
color: theme.palette.text.primary,
48+
borderRadius: theme.shape.borderRadius + 'px',
49+
backgroundColor: theme.palette.background.muted,
50+
border: '1px solid',
51+
borderColor: theme.palette.divider,
52+
fontSize: theme.typography.pxToRem(14),
53+
padding: '4px 8px',
54+
marginRight: theme.spacing(1),
55+
whiteSpace: 'nowrap',
56+
display: 'inline-block',
57+
maxWidth: '400px',
58+
overflow: 'hidden',
59+
textOverflow: 'ellipsis',
60+
})}
61+
ref={ref}
62+
/>
63+
);
64+
});
65+
EnvEntry.displayName = 'EnvEntry';
66+
67+
const renderEnvVar = (envVar: NonNullable<KubeContainer['env']>[number]) => {
68+
// Fallback:
69+
if (!envVar.name) {
70+
return null;
71+
}
72+
// Secret Key:
73+
if (envVar.valueFrom?.secretKeyRef) {
74+
const { name: secretName, key: secretKey } = envVar.valueFrom.secretKeyRef;
75+
76+
if (!secretName) {
77+
return (
78+
<EnvEntry component="span" key={envVar.name}>
79+
{envVar.name}: {t('translation|Invalid Secret Reference')}
80+
</EnvEntry>
81+
);
82+
}
83+
84+
const secretUrl = `/c/${encodeURIComponent(cluster)}/secrets/${encodeURIComponent(
85+
namespace
86+
)}/${encodeURIComponent(secretName)}`;
87+
88+
return (
89+
<EnvEntry component="span" key={envVar.name}>
90+
{envVar.name}:{' '}
91+
<Link to={secretUrl} style={{ textDecoration: 'underline', fontWeight: 'bold' }}>
92+
Secret: {secretName} {secretKey ? `(Key: ${secretKey})` : ''}
93+
</Link>
94+
</EnvEntry>
95+
);
96+
}
97+
98+
// Config Map:
99+
if (envVar.valueFrom?.configMapKeyRef) {
100+
const { name: cmName, key: cmKey } = envVar.valueFrom.configMapKeyRef;
101+
102+
if (!cmName) {
103+
return (
104+
<EnvEntry component="span" key={envVar.name}>
105+
{envVar.name}: {t('translation|Invalid Config Map Reference')}
106+
</EnvEntry>
107+
);
108+
}
109+
110+
const configMapUrl = `/c/${encodeURIComponent(cluster)}/configmaps/${encodeURIComponent(
111+
namespace
112+
)}/${encodeURIComponent(cmName)}`;
113+
return (
114+
<EnvEntry component="span" key={envVar.name}>
115+
{envVar.name}:{' '}
116+
<Link to={configMapUrl} style={{ textDecoration: 'underline', fontWeight: 'bold' }}>
117+
ConfigMap: {cmName} {cmKey ? `(Key: ${cmKey})` : ''}
118+
</Link>
119+
</EnvEntry>
120+
);
121+
}
122+
123+
// FieldRef:
124+
if (envVar.valueFrom?.fieldRef) {
125+
const { fieldPath } = envVar.valueFrom.fieldRef;
126+
return (
127+
<EnvEntry component="span" key={envVar.name}>
128+
{envVar.name}: FieldRef {fieldPath ? `(${fieldPath})` : t('translation|Invalid fieldRef')}
129+
</EnvEntry>
130+
);
131+
}
132+
133+
// ResourceFieldRef:
134+
if (envVar.valueFrom?.resourceFieldRef) {
135+
const { resource } = envVar.valueFrom.resourceFieldRef;
136+
return (
137+
<EnvEntry component="span" key={envVar.name}>
138+
{envVar.name}: ResourceField{' '}
139+
{resource ? `(${resource})` : t('translation|Invalid resourceFieldRef')}
140+
</EnvEntry>
141+
);
142+
}
143+
144+
// Plaintext
145+
return (
146+
<EnvEntry component="span" key={envVar.name}>
147+
{envVar.name}: {(envVar.value || '').trim()}
148+
</EnvEntry>
149+
);
150+
};
151+
152+
return (
153+
<Box>
154+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
155+
{validEnvVars
156+
.slice(0, expanded ? validEnvVars.length : defaultNumShown)
157+
.map(env => renderEnvVar(env))}
158+
</Box>
159+
{validEnvVars.length > defaultNumShown && (
160+
<Button
161+
onClick={() => setExpanded(!expanded)}
162+
size="small"
163+
aria-expanded={expanded}
164+
aria-label={
165+
expanded ? t('translation|Show fewer') : t('translation|Show all environment variables')
166+
}
167+
startIcon={<Icon icon={expanded ? 'mdi:menu-up' : 'mdi:menu-down'} />}
168+
sx={{ mt: 1, mb: 1 }}
169+
>
170+
{!expanded
171+
? t('translation|Show all environment variables (+{{count}} more)', {
172+
count: validEnvVars.length - defaultNumShown,
173+
})
174+
: t('translation|Show fewer')}
175+
</Button>
176+
)}
177+
</Box>
178+
);
179+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<body>
2+
<div>
3+
<div
4+
class="MuiBox-root css-0"
5+
>
6+
<div
7+
class="MuiBox-root css-yi3mkw"
8+
>
9+
<span
10+
class="MuiTypography-root MuiTypography-body1 css-yecqev-MuiTypography-root"
11+
>
12+
API_KEY
13+
:
14+
15+
<a
16+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineHover css-2ugbm1-MuiTypography-root-MuiLink-root"
17+
href="/c/minikube/secrets/default/my-secret"
18+
style="text-decoration: underline; font-weight: bold;"
19+
>
20+
Secret:
21+
my-secret
22+
23+
(Key: api-key)
24+
</a>
25+
</span>
26+
<span
27+
class="MuiTypography-root MuiTypography-body1 css-yecqev-MuiTypography-root"
28+
>
29+
APP_CONFIG
30+
:
31+
32+
<a
33+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineHover css-2ugbm1-MuiTypography-root-MuiLink-root"
34+
href="/c/minikube/configmaps/default/app-config"
35+
style="text-decoration: underline; font-weight: bold;"
36+
>
37+
ConfigMap:
38+
app-config
39+
40+
(Key: config.json)
41+
</a>
42+
</span>
43+
<span
44+
class="MuiTypography-root MuiTypography-body1 css-yecqev-MuiTypography-root"
45+
>
46+
MY_POD_IP
47+
: FieldRef
48+
(status.podIP)
49+
</span>
50+
<span
51+
class="MuiTypography-root MuiTypography-body1 css-yecqev-MuiTypography-root"
52+
>
53+
CPU_LIMIT
54+
: ResourceField
55+
56+
(limits.cpu)
57+
</span>
58+
</div>
59+
</div>
60+
</div>
61+
</body>

0 commit comments

Comments
 (0)