Skip to content

Commit 6739344

Browse files
committed
frontend: clusters: add cluster appearance local storage settings
1 parent d9a3326 commit 6739344

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1389
-350
lines changed

frontend/src/components/App/Home/ClusterTable.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Typography from '@mui/material/Typography';
2222
import { useMemo } from 'react';
2323
import { useTranslation } from 'react-i18next';
2424
import { generatePath, useHistory } from 'react-router-dom';
25+
import { getClusterAppearanceFromMeta } from '../../../helpers/clusterAppearance';
2526
import { isElectron } from '../../../helpers/isElectron';
2627
import { formatClusterPathParam } from '../../../lib/cluster';
2728
import { useClustersConf, useClustersVersion } from '../../../lib/k8s';
@@ -33,6 +34,7 @@ import { useTypedSelector } from '../../../redux/hooks';
3334
import { Loader } from '../../common';
3435
import Link from '../../common/Link';
3536
import Table from '../../common/Table';
37+
import ClusterBadge from '../../Sidebar/ClusterBadge';
3638
import ClusterContextMenu from './ClusterContextMenu';
3739
import { MULTI_HOME_ENABLED } from './config';
3840
import { getCustomClusterNames } from './customClusterNames';
@@ -190,11 +192,18 @@ export default function ClusterTable({
190192
id: 'name',
191193
header: t('Name'),
192194
accessorKey: 'name',
193-
Cell: ({ row: { original } }) => (
194-
<Link routeName="cluster" params={{ cluster: original.name }}>
195-
{original.name}
196-
</Link>
197-
),
195+
Cell: ({ row: { original } }) => {
196+
const appearance = getClusterAppearanceFromMeta(original.name);
197+
return (
198+
<Link routeName="cluster" params={{ cluster: original.name }}>
199+
<ClusterBadge
200+
name={original.name}
201+
icon={appearance.icon}
202+
accentColor={appearance.accentColor}
203+
/>
204+
</Link>
205+
);
206+
},
198207
},
199208
{
200209
header: t('Origin'),

frontend/src/components/App/Home/RecentClusters.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { styled } from '@mui/system';
2222
import React from 'react';
2323
import { useTranslation } from 'react-i18next';
2424
import { generatePath, useHistory } from 'react-router-dom';
25+
import { getClusterAppearanceFromMeta } from '../../../helpers/clusterAppearance';
2526
import { isElectron } from '../../../helpers/isElectron';
2627
import { getRecentClusters, setRecentCluster } from '../../../helpers/recentClusters';
2728
import { formatClusterPathParam, getClusterPrefixedPath } from '../../../lib/cluster';
@@ -44,11 +45,14 @@ interface ClusterButtonProps extends React.PropsWithChildren<{}> {
4445

4546
function ClusterButton(props: ClusterButtonProps) {
4647
const { cluster, onClick = undefined, focusedRef } = props;
48+
const appearance = getClusterAppearanceFromMeta(cluster?.name || '');
49+
const icon = appearance.icon || 'mdi:kubernetes';
4750

4851
return (
4952
<SquareButton
5053
focusRipple
51-
icon="mdi:kubernetes"
54+
icon={icon}
55+
iconColor={appearance.accentColor}
5256
label={cluster.name}
5357
ref={focusedRef}
5458
onClick={onClick}

frontend/src/components/App/Home/__snapshots__/index.Base.stories.storyshot

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,18 @@
617617
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineHover css-2ugbm1-MuiTypography-root-MuiLink-root"
618618
href="/c/cluster0/"
619619
>
620-
cluster0
620+
<div
621+
class="MuiBox-root css-1or8la4"
622+
>
623+
<div
624+
class="MuiBox-root css-iyhyxy"
625+
/>
626+
<span
627+
class="MuiTypography-root MuiTypography-caption css-chqac-MuiTypography-root"
628+
>
629+
cluster0
630+
</span>
631+
</div>
621632
</a>
622633
</td>
623634
<td
@@ -717,7 +728,18 @@
717728
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineHover css-2ugbm1-MuiTypography-root-MuiLink-root"
718729
href="/c/cluster1/"
719730
>
720-
cluster1
731+
<div
732+
class="MuiBox-root css-1or8la4"
733+
>
734+
<div
735+
class="MuiBox-root css-iyhyxy"
736+
/>
737+
<span
738+
class="MuiTypography-root MuiTypography-caption css-chqac-MuiTypography-root"
739+
>
740+
cluster1
741+
</span>
742+
</div>
721743
</a>
722744
</td>
723745
<td
@@ -817,7 +839,18 @@
817839
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineHover css-2ugbm1-MuiTypography-root-MuiLink-root"
818840
href="/c/cluster2/"
819841
>
820-
cluster2
842+
<div
843+
class="MuiBox-root css-1or8la4"
844+
>
845+
<div
846+
class="MuiBox-root css-iyhyxy"
847+
/>
848+
<span
849+
class="MuiTypography-root MuiTypography-caption css-chqac-MuiTypography-root"
850+
>
851+
cluster2
852+
</span>
853+
</div>
821854
</a>
822855
</td>
823856
<td
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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 Box from '@mui/material/Box';
18+
import Button from '@mui/material/Button';
19+
import Checkbox from '@mui/material/Checkbox';
20+
import Dialog from '@mui/material/Dialog';
21+
import DialogActions from '@mui/material/DialogActions';
22+
import DialogContent from '@mui/material/DialogContent';
23+
import DialogTitle from '@mui/material/DialogTitle';
24+
import FormControlLabel from '@mui/material/FormControlLabel';
25+
import { useTheme } from '@mui/material/styles';
26+
import TextField from '@mui/material/TextField';
27+
import ToggleButton from '@mui/material/ToggleButton';
28+
import Tooltip from '@mui/material/Tooltip';
29+
import React from 'react';
30+
import { useTranslation } from 'react-i18next';
31+
import { isValidCssColor } from '../../../helpers/clusterAppearance';
32+
33+
// Preset colors for cluster appearance
34+
export const PRESET_COLORS = [
35+
{ name: 'Red', value: '#f44336' },
36+
{ name: 'Pink', value: '#e91e63' },
37+
{ name: 'Purple', value: '#9c27b0' },
38+
{ name: 'Deep Purple', value: '#673ab7' },
39+
{ name: 'Indigo', value: '#3f51b5' },
40+
{ name: 'Blue', value: '#2196f3' },
41+
{ name: 'Light Blue', value: '#03a9f4' },
42+
{ name: 'Cyan', value: '#00bcd4' },
43+
{ name: 'Teal', value: '#009688' },
44+
{ name: 'Green', value: '#4caf50' },
45+
{ name: 'Light Green', value: '#8bc34a' },
46+
{ name: 'Lime', value: '#cddc39' },
47+
{ name: 'Yellow', value: '#ffeb3b' },
48+
{ name: 'Amber', value: '#ffc107' },
49+
{ name: 'Orange', value: '#ff9800' },
50+
{ name: 'Deep Orange', value: '#ff5722' },
51+
];
52+
53+
interface ColorPickerProps {
54+
open: boolean;
55+
currentColor: string;
56+
onClose: () => void;
57+
onSelectColor: (color: string) => void;
58+
onError: (error: string) => void;
59+
}
60+
61+
export default function ColorPicker({
62+
open,
63+
currentColor,
64+
onClose,
65+
onSelectColor,
66+
onError,
67+
}: ColorPickerProps) {
68+
const { t } = useTranslation(['translation']);
69+
const theme = useTheme();
70+
71+
const [customColorInput, setCustomColorInput] = React.useState('');
72+
const [useCustomColor, setUseCustomColor] = React.useState(false);
73+
74+
const isValidAccentColor = (color: string): boolean => !color || isValidCssColor(color);
75+
76+
const handlePresetColorClick = (color: string) => {
77+
setUseCustomColor(false);
78+
onSelectColor(color);
79+
onError('');
80+
onClose();
81+
};
82+
83+
const handleApplyCustomColor = () => {
84+
if (isValidAccentColor(customColorInput)) {
85+
onSelectColor(customColorInput);
86+
onError('');
87+
onClose();
88+
} else {
89+
onError(
90+
t('translation|Accent color format is invalid. Use hex (#ff0000), rgb(), or rgba().')
91+
);
92+
}
93+
};
94+
95+
return (
96+
<Dialog open={open} onClose={onClose} maxWidth="sm">
97+
<DialogTitle>{t('translation|Choose Accent Color')}</DialogTitle>
98+
<DialogContent>
99+
<Box sx={{ pt: 1 }}>
100+
<Box display="flex" flexWrap="wrap" gap={1}>
101+
{PRESET_COLORS.map(color => (
102+
<Tooltip key={color.value} title={color.name}>
103+
<ToggleButton
104+
value={color.value}
105+
selected={currentColor === color.value && !useCustomColor}
106+
onChange={() => handlePresetColorClick(color.value)}
107+
disabled={useCustomColor}
108+
sx={{
109+
width: 48,
110+
height: 48,
111+
backgroundColor: color.value,
112+
'&:hover': {
113+
backgroundColor: color.value,
114+
opacity: 0.8,
115+
},
116+
'&.Mui-selected': {
117+
backgroundColor: color.value,
118+
border: `3px solid ${theme.palette.text.primary}`,
119+
'&:hover': {
120+
backgroundColor: color.value,
121+
},
122+
},
123+
'&.Mui-disabled': {
124+
opacity: 0.5,
125+
},
126+
}}
127+
/>
128+
</Tooltip>
129+
))}
130+
</Box>
131+
</Box>
132+
<Box sx={{ mt: 2 }}>
133+
<FormControlLabel
134+
control={
135+
<Checkbox
136+
checked={useCustomColor}
137+
onChange={e => setUseCustomColor(e.target.checked)}
138+
/>
139+
}
140+
label={t('translation|Use custom color')}
141+
/>
142+
{useCustomColor && (
143+
<TextField
144+
label={t('translation|Custom color')}
145+
placeholder="#ff0000"
146+
value={customColorInput}
147+
onChange={e => setCustomColorInput(e.target.value)}
148+
fullWidth
149+
helperText={t('translation|Hex, rgb(), or rgba()')}
150+
error={!!customColorInput && !isValidAccentColor(customColorInput)}
151+
sx={{ mt: 1 }}
152+
/>
153+
)}
154+
</Box>
155+
</DialogContent>
156+
<DialogActions>
157+
<Button onClick={onClose}>{t('translation|Cancel')}</Button>
158+
{useCustomColor && (
159+
<Button
160+
onClick={handleApplyCustomColor}
161+
disabled={!customColorInput || !isValidAccentColor(customColorInput)}
162+
>
163+
{t('translation|Apply')}
164+
</Button>
165+
)}
166+
</DialogActions>
167+
</Dialog>
168+
);
169+
}

0 commit comments

Comments
 (0)