Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ff73cc9

Browse files
committedFeb 7, 2025·
refactor and add test stories
Signed-off-by: Faakhir30 <zahidfaakhir@gmail.com>
1 parent 811df29 commit ff73cc9

File tree

4 files changed

+268
-57
lines changed

4 files changed

+268
-57
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Meta, StoryObj } from '@storybook/react';
2+
import { ClusterNameEditor } from './ClusterNameEditor';
3+
4+
const meta: Meta<typeof ClusterNameEditor> = {
5+
title: 'Settings/ClusterNameEditor',
6+
component: ClusterNameEditor,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
};
11+
12+
export default meta;
13+
type Story = StoryObj<typeof ClusterNameEditor>;
14+
15+
export const Default: Story = {
16+
args: {
17+
cluster: 'my-cluster',
18+
newClusterName: '',
19+
isValidCurrentName: true,
20+
source: 'dynamic_cluster',
21+
onClusterNameChange: () => {},
22+
onUpdateClusterName: () => {},
23+
},
24+
};
25+
26+
export const WithInvalidName: Story = {
27+
args: {
28+
...Default.args,
29+
newClusterName: 'Invalid Cluster Name',
30+
isValidCurrentName: false,
31+
},
32+
};
33+
34+
export const WithNewName: Story = {
35+
args: {
36+
...Default.args,
37+
newClusterName: 'new-cluster-name',
38+
},
39+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Box, TextField } from '@mui/material';
2+
import React from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
import { ConfirmButton,NameValueTable } from '../../common';
5+
6+
interface ClusterNameEditorProps {
7+
cluster: string;
8+
newClusterName: string;
9+
isValidCurrentName: boolean;
10+
source: string;
11+
onClusterNameChange: (name: string) => void;
12+
onUpdateClusterName: (source: string) => void;
13+
}
14+
15+
export function ClusterNameEditor({
16+
cluster,
17+
newClusterName,
18+
isValidCurrentName,
19+
source,
20+
onClusterNameChange,
21+
onUpdateClusterName,
22+
}: ClusterNameEditorProps) {
23+
const { t } = useTranslation(['translation']);
24+
25+
const invalidClusterNameMessage = t(
26+
"translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character."
27+
);
28+
29+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
30+
const value = event.target.value.replace(' ', '');
31+
onClusterNameChange(value);
32+
};
33+
34+
const handleKeyPress = (event: React.KeyboardEvent) => {
35+
if (event.key === 'Enter' && isValidCurrentName) {
36+
onUpdateClusterName(source);
37+
}
38+
};
39+
40+
return (
41+
<NameValueTable
42+
rows={[
43+
{
44+
name: t('translation|Name'),
45+
value: (
46+
<TextField
47+
onChange={handleChange}
48+
value={newClusterName}
49+
placeholder={cluster}
50+
error={!isValidCurrentName}
51+
helperText={
52+
isValidCurrentName
53+
? t(
54+
'translation|The current name of cluster. You can define custom modified name.'
55+
)
56+
: invalidClusterNameMessage
57+
}
58+
InputProps={{
59+
endAdornment: (
60+
<Box pt={2} textAlign="right">
61+
<ConfirmButton
62+
onConfirm={() => {
63+
if (isValidCurrentName) {
64+
onUpdateClusterName(source);
65+
}
66+
}}
67+
confirmTitle={t('translation|Change name')}
68+
confirmDescription={t(
69+
'translation|Are you sure you want to change the name for "{{ clusterName }}"?',
70+
{ clusterName: cluster }
71+
)}
72+
disabled={!newClusterName || !isValidCurrentName}
73+
>
74+
{t('translation|Apply')}
75+
</ConfirmButton>
76+
</Box>
77+
),
78+
onKeyPress: handleKeyPress,
79+
autoComplete: 'off',
80+
sx: { maxWidth: 250 },
81+
}}
82+
/>
83+
),
84+
},
85+
]}
86+
/>
87+
);
88+
}

Diff for: ‎frontend/src/components/App/Settings/SettingsCluster.tsx

+8-57
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { findKubeconfigByClusterName, updateStatelessClusterKubeconfig } from '.
2222
import { Link, Loader, NameValueTable, SectionBox } from '../../common';
2323
import ConfirmButton from '../../common/ConfirmButton';
2424
import Empty from '../../common/EmptyContent';
25+
import { ClusterNameEditor } from './ClusterNameEditor';
2526

2627
function isValidNamespaceFormat(namespace: string) {
2728
// We allow empty strings just because that's the default value in our case.
@@ -272,10 +273,6 @@ export default function SettingsCluster() {
272273
"translation|Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character."
273274
);
274275

275-
const invalidClusterNameMessage = t(
276-
"translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character."
277-
);
278-
279276
// If we don't have yet a cluster name from the URL, we are still loading.
280277
if (!clusterFromURLRef.current) {
281278
return <Loader title="Loading" />;
@@ -333,59 +330,13 @@ export default function SettingsCluster() {
333330
</Link>
334331
</Box>
335332
{helpers.isElectron() && (
336-
<NameValueTable
337-
rows={[
338-
{
339-
name: t('translation|Name'),
340-
value: (
341-
<TextField
342-
onChange={event => {
343-
let value = event.target.value;
344-
value = value.replace(' ', '');
345-
setNewClusterName(value);
346-
}}
347-
value={newClusterName}
348-
placeholder={cluster}
349-
error={!isValidCurrentName}
350-
helperText={
351-
isValidCurrentName
352-
? t(
353-
'translation|The current name of cluster. You can define custom modified name.'
354-
)
355-
: invalidClusterNameMessage
356-
}
357-
InputProps={{
358-
endAdornment: (
359-
<Box pt={2} textAlign="right">
360-
<ConfirmButton
361-
onConfirm={() => {
362-
if (isValidCurrentName) {
363-
handleUpdateClusterName(source);
364-
}
365-
}}
366-
confirmTitle={t('translation|Change name')}
367-
confirmDescription={t(
368-
'translation|Are you sure you want to change the name for "{{ clusterName }}"?',
369-
{ clusterName: cluster }
370-
)}
371-
disabled={!newClusterName || !isValidCurrentName}
372-
>
373-
{t('translation|Apply')}
374-
</ConfirmButton>
375-
</Box>
376-
),
377-
onKeyPress: event => {
378-
if (event.key === 'Enter' && isValidCurrentName) {
379-
handleUpdateClusterName(source);
380-
}
381-
},
382-
autoComplete: 'off',
383-
sx: { maxWidth: 250 },
384-
}}
385-
/>
386-
),
387-
},
388-
]}
333+
<ClusterNameEditor
334+
cluster={cluster}
335+
newClusterName={newClusterName}
336+
isValidCurrentName={isValidCurrentName}
337+
source={source}
338+
onClusterNameChange={setNewClusterName}
339+
onUpdateClusterName={handleUpdateClusterName}
389340
/>
390341
)}
391342
<NameValueTable

Diff for: ‎frontend/src/stateless/index.test.ts

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import * as jsyaml from 'js-yaml';
2+
import { beforeAll, describe, expect, it } from 'vitest';
3+
import { updateStatelessClusterKubeconfig } from './index';
4+
5+
describe('updateStatelessClusterKubeconfig', () => {
6+
let indexedDB: IDBFactory;
7+
8+
beforeAll(() => {
9+
// Mock IndexedDB
10+
indexedDB = {
11+
open: jest.fn(),
12+
} as any;
13+
global.indexedDB = indexedDB;
14+
});
15+
16+
it('should update existing kubeconfig with new custom name', async () => {
17+
const mockKubeconfig = {
18+
contexts: [
19+
{
20+
name: 'test-cluster',
21+
context: {
22+
cluster: 'test-cluster',
23+
user: 'test-user',
24+
},
25+
},
26+
],
27+
};
28+
29+
const base64Kubeconfig = btoa(jsyaml.dump(mockKubeconfig));
30+
31+
const mockStore = {
32+
put: jest.fn().mockImplementation(() => ({
33+
onsuccess: jest.fn(),
34+
})),
35+
openCursor: jest.fn().mockImplementation(() => ({
36+
onsuccess: jest.fn(),
37+
})),
38+
};
39+
40+
const mockTransaction = {
41+
objectStore: jest.fn().mockReturnValue(mockStore),
42+
};
43+
44+
const mockDB = {
45+
transaction: jest.fn().mockReturnValue(mockTransaction),
46+
};
47+
48+
(indexedDB.open as jest.Mock).mockImplementation(() => ({
49+
onsuccess: function (this: any) {
50+
this.result = mockDB;
51+
this.onsuccess();
52+
},
53+
}));
54+
55+
await updateStatelessClusterKubeconfig(base64Kubeconfig, 'new-name', 'test-cluster');
56+
57+
// Verify the kubeconfig was updated with the new custom name
58+
expect(mockStore.put).toHaveBeenCalled();
59+
});
60+
61+
it('should reject if no matching context is found', async () => {
62+
const mockKubeconfig = {
63+
contexts: [
64+
{
65+
name: 'different-cluster',
66+
context: {
67+
cluster: 'different-cluster',
68+
user: 'test-user',
69+
},
70+
},
71+
],
72+
};
73+
74+
const base64Kubeconfig = btoa(jsyaml.dump(mockKubeconfig));
75+
76+
await expect(
77+
updateStatelessClusterKubeconfig(base64Kubeconfig, 'new-name', 'test-cluster')
78+
).rejects.toEqual('No context found matching the cluster name');
79+
});
80+
81+
it('should update existing headlamp_info extension', async () => {
82+
const mockKubeconfig = {
83+
contexts: [
84+
{
85+
name: 'test-cluster',
86+
context: {
87+
cluster: 'test-cluster',
88+
user: 'test-user',
89+
extensions: [
90+
{
91+
name: 'headlamp_info',
92+
extension: {
93+
customName: 'old-name',
94+
},
95+
},
96+
],
97+
},
98+
},
99+
],
100+
};
101+
102+
const base64Kubeconfig = btoa(jsyaml.dump(mockKubeconfig));
103+
104+
const mockStore = {
105+
put: jest.fn().mockImplementation(() => ({
106+
onsuccess: jest.fn(),
107+
})),
108+
openCursor: jest.fn().mockImplementation(() => ({
109+
onsuccess: jest.fn(),
110+
})),
111+
};
112+
113+
const mockTransaction = {
114+
objectStore: jest.fn().mockReturnValue(mockStore),
115+
};
116+
117+
const mockDB = {
118+
transaction: jest.fn().mockReturnValue(mockTransaction),
119+
};
120+
121+
(indexedDB.open as jest.Mock).mockImplementation(() => ({
122+
onsuccess: function (this: any) {
123+
this.result = mockDB;
124+
this.onsuccess();
125+
},
126+
}));
127+
128+
await updateStatelessClusterKubeconfig(base64Kubeconfig, 'new-name', 'test-cluster');
129+
130+
// Verify the kubeconfig was updated with the new custom name
131+
expect(mockStore.put).toHaveBeenCalled();
132+
});
133+
});

0 commit comments

Comments
 (0)
Please sign in to comment.