Skip to content

Commit 3ad7cab

Browse files
frontend: SettingsCluster stories for cluster settings states
1 parent 51c2b8b commit 3ad7cab

12 files changed

+6432
-0
lines changed
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
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 { configureStore } from '@reduxjs/toolkit';
18+
import { Meta, StoryFn } from '@storybook/react';
19+
import React from 'react';
20+
import { TestContext } from '../../../test';
21+
import SettingsCluster from './SettingsCluster';
22+
23+
const mockClusterName = 'my-cluster';
24+
25+
function setupLocalStorage(clusterName: string, settings: Record<string, any> = {}) {
26+
localStorage.setItem(`cluster_settings.${clusterName}`, JSON.stringify(settings));
27+
}
28+
29+
function getMockStore(clusters: Record<string, any> = {}) {
30+
return configureStore({
31+
reducer: {
32+
config: (
33+
state = {
34+
clusters,
35+
statelessClusters: {},
36+
allClusters: clusters,
37+
settings: {
38+
tableRowsPerPageOptions: [15, 25, 50],
39+
timezone: 'UTC',
40+
useEvict: true,
41+
},
42+
}
43+
) => state,
44+
plugins: (state = { loaded: true }) => state,
45+
theme: (
46+
state = {
47+
name: 'light',
48+
logo: null,
49+
palette: { navbar: { background: '#fff' } },
50+
}
51+
) => state,
52+
ui: (state = {}) => state,
53+
filter: (
54+
state = {
55+
namespaces: new Set(),
56+
search: '',
57+
}
58+
) => state,
59+
resourceTable: (state = {}) => state,
60+
actionButtons: (state = []) => state,
61+
detailsViewSection: (state = {}) => state,
62+
routes: (state = { routes: {} }) => state,
63+
notifications: (state = { notifications: [] }) => state,
64+
},
65+
});
66+
}
67+
68+
const mockClusters: Record<string, any> = {
69+
[mockClusterName]: {
70+
name: mockClusterName,
71+
auth_type: '',
72+
meta_data: {
73+
namespace: 'default',
74+
source: 'kubeconfig',
75+
},
76+
},
77+
'staging-cluster': {
78+
name: 'staging-cluster',
79+
auth_type: '',
80+
meta_data: {
81+
namespace: 'default',
82+
source: 'kubeconfig',
83+
},
84+
},
85+
};
86+
87+
export default {
88+
title: 'Settings/SettingsCluster',
89+
component: SettingsCluster,
90+
} as Meta<typeof SettingsCluster>;
91+
92+
/**
93+
* Default cluster settings form with a selected cluster.
94+
*/
95+
export const Default: StoryFn = () => {
96+
setupLocalStorage(mockClusterName, {
97+
defaultNamespace: '',
98+
allowedNamespaces: [],
99+
});
100+
101+
return (
102+
<TestContext store={getMockStore(mockClusters)} urlSearchParams={{ c: mockClusterName }}>
103+
<SettingsCluster />
104+
</TestContext>
105+
);
106+
};
107+
108+
/**
109+
* Cluster settings form pre-populated with saved settings (allowed namespaces, default namespace).
110+
*/
111+
export const WithSavedSettings: StoryFn = () => {
112+
setupLocalStorage(mockClusterName, {
113+
defaultNamespace: 'my-app',
114+
allowedNamespaces: ['default', 'kube-system', 'my-app'],
115+
nodeShellTerminal: {
116+
isEnabled: true,
117+
namespace: 'kube-system',
118+
linuxImage: 'busybox:1.28',
119+
},
120+
podDebugTerminal: {
121+
isEnabled: true,
122+
debugImage: 'docker.io/library/busybox:latest',
123+
},
124+
appearance: {
125+
accentColor: '#1976d2',
126+
icon: 'mdi:kubernetes',
127+
},
128+
});
129+
130+
return (
131+
<TestContext store={getMockStore(mockClusters)} urlSearchParams={{ c: mockClusterName }}>
132+
<SettingsCluster />
133+
</TestContext>
134+
);
135+
};
136+
137+
/**
138+
* No clusters configured — shows empty state message.
139+
*/
140+
export const NoClusters: StoryFn = () => {
141+
return (
142+
<TestContext store={getMockStore({})} urlSearchParams={{ c: 'nonexistent' }}>
143+
<SettingsCluster />
144+
</TestContext>
145+
);
146+
};
147+
148+
/**
149+
* Selected cluster does not exist — shows error with cluster selector.
150+
*/
151+
export const InvalidCluster: StoryFn = () => {
152+
return (
153+
<TestContext store={getMockStore(mockClusters)} urlSearchParams={{ c: 'nonexistent-cluster' }}>
154+
<SettingsCluster />
155+
</TestContext>
156+
);
157+
};
158+
159+
/**
160+
* Cluster settings showing namespace validation error.
161+
* Note: Type an invalid namespace (with spaces or capitals) in the "Default namespace"
162+
* or "Allowed namespaces" field to see the validation error message in action.
163+
* This story demonstrates the initial state ready for validation testing.
164+
*/
165+
export const NamespaceValidation: StoryFn = () => {
166+
setupLocalStorage(mockClusterName, {
167+
defaultNamespace: '',
168+
allowedNamespaces: [],
169+
});
170+
171+
return (
172+
<TestContext store={getMockStore(mockClusters)} urlSearchParams={{ c: mockClusterName }}>
173+
<SettingsCluster />
174+
</TestContext>
175+
);
176+
};
177+
178+
/**
179+
* Cluster settings with appearance customization (accent color and icon set).
180+
*/
181+
export const WithAppearance: StoryFn = () => {
182+
setupLocalStorage(mockClusterName, {
183+
defaultNamespace: '',
184+
allowedNamespaces: [],
185+
appearance: {
186+
accentColor: '#e91e63',
187+
icon: 'mdi:cloud-outline',
188+
},
189+
});
190+
191+
return (
192+
<TestContext store={getMockStore(mockClusters)} urlSearchParams={{ c: mockClusterName }}>
193+
<SettingsCluster />
194+
</TestContext>
195+
);
196+
};
197+
198+
/**
199+
* Dynamic cluster (removable) — would show remove button in Electron.
200+
*/
201+
export const DynamicCluster: StoryFn = () => {
202+
const dynamicClusters = {
203+
[mockClusterName]: {
204+
name: mockClusterName,
205+
auth_type: '',
206+
meta_data: {
207+
namespace: 'default',
208+
source: 'dynamic_cluster',
209+
},
210+
},
211+
};
212+
213+
setupLocalStorage(mockClusterName, {
214+
defaultNamespace: 'production',
215+
allowedNamespaces: ['production', 'staging'],
216+
});
217+
218+
return (
219+
<TestContext store={getMockStore(dynamicClusters)} urlSearchParams={{ c: mockClusterName }}>
220+
<SettingsCluster />
221+
</TestContext>
222+
);
223+
};
224+
225+
/**
226+
* Multiple allowed namespaces displayed as chips.
227+
*/
228+
export const MultipleAllowedNamespaces: StoryFn = () => {
229+
setupLocalStorage(mockClusterName, {
230+
defaultNamespace: 'default',
231+
allowedNamespaces: [
232+
'default',
233+
'kube-system',
234+
'monitoring',
235+
'logging',
236+
'ingress-nginx',
237+
'cert-manager',
238+
],
239+
});
240+
241+
return (
242+
<TestContext store={getMockStore(mockClusters)} urlSearchParams={{ c: mockClusterName }}>
243+
<SettingsCluster />
244+
</TestContext>
245+
);
246+
};
247+
248+
/**
249+
* Demonstrates the appearance save loading state.
250+
* To see the loading state in action:
251+
* 1. Choose a color or icon using the pickers
252+
* 2. Click the "Apply" button
253+
* 3. The button will briefly show "Applying..." while saving
254+
*
255+
* This story shows the component ready for interaction.
256+
*/
257+
export const AppearanceSaving: StoryFn = () => {
258+
setupLocalStorage(mockClusterName, {
259+
defaultNamespace: '',
260+
allowedNamespaces: [],
261+
appearance: {
262+
accentColor: '#1976d2',
263+
icon: 'mdi:kubernetes',
264+
},
265+
});
266+
267+
return (
268+
<TestContext store={getMockStore(mockClusters)} urlSearchParams={{ c: mockClusterName }}>
269+
<SettingsCluster />
270+
</TestContext>
271+
);
272+
};
273+
274+
AppearanceSaving.parameters = {
275+
docs: {
276+
description: {
277+
story:
278+
'Click the "Apply" button to see the loading state. ' +
279+
'The button text changes to "Applying..." and becomes disabled during the save operation.',
280+
},
281+
},
282+
};
283+
284+
/**
285+
* Demonstrates appearance save error state.
286+
* To see the error message in action:
287+
* 1. Manually type an invalid color value (e.g., "invalid-color", "###", "badrgb()")
288+
* 2. Click the "Apply" button
289+
* 3. An error message will appear: "Accent color format is invalid..."
290+
*
291+
* This story shows the component ready for error demonstration.
292+
*/
293+
export const AppearanceSaveError: StoryFn = () => {
294+
setupLocalStorage(mockClusterName, {
295+
defaultNamespace: '',
296+
allowedNamespaces: [],
297+
});
298+
299+
return (
300+
<TestContext store={getMockStore(mockClusters)} urlSearchParams={{ c: mockClusterName }}>
301+
<SettingsCluster />
302+
</TestContext>
303+
);
304+
};
305+
306+
AppearanceSaveError.parameters = {
307+
docs: {
308+
description: {
309+
story:
310+
'Try entering invalid color formats to see validation errors: ' +
311+
'Examples of invalid formats: "xyz", "notacolor", "###", "rgb(999,999,999)". ' +
312+
'Valid formats include: hex (#ff0000), rgb(255,0,0), rgba(255,0,0,0.5), or CSS color names (red, blue).',
313+
},
314+
},
315+
};
316+
317+
/**
318+
* Demonstrates appearance save success state.
319+
* Shows the form after successfully applying appearance changes.
320+
*/
321+
export const AppearanceSaveSuccess: StoryFn = () => {
322+
setupLocalStorage(mockClusterName, {
323+
defaultNamespace: 'my-app',
324+
allowedNamespaces: ['default', 'my-app'],
325+
appearance: {
326+
accentColor: '#4caf50',
327+
icon: 'mdi:check-circle',
328+
},
329+
});
330+
331+
return (
332+
<TestContext store={getMockStore(mockClusters)} urlSearchParams={{ c: mockClusterName }}>
333+
<SettingsCluster />
334+
</TestContext>
335+
);
336+
};
337+
338+
// Add documentation note for AppearanceSaveSuccess
339+
AppearanceSaveSuccess.parameters = {
340+
docs: {
341+
description: {
342+
story:
343+
'This story shows the component state after a successful save. ' +
344+
'The appearance settings (green color and check icon) have been applied. ' +
345+
'Note: The component does not show an explicit success message; ' +
346+
'success is indicated by the settings being persisted and the button returning to "Apply" state.',
347+
},
348+
},
349+
};

0 commit comments

Comments
 (0)