Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend: Settings: refactor and test Rename Cluster #2871

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions app/e2e-tests/tests/clusterRename.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { expect, test } from '@playwright/test';
import path from 'path';
import { _electron, Page } from 'playwright';
import { HeadlampPage } from './headlampPage';

// Electron setup
const electronExecutable = process.platform === 'win32' ? 'electron.cmd' : 'electron';
const electronPath = path.resolve(__dirname, `../../node_modules/.bin/${electronExecutable}`);
const appPath = path.resolve(__dirname, '../../');
let electronApp;
let electronPage: Page;

// Test configuration
const TEST_CONFIG = {
originalName: 'minikube',
newName: 'test-cluster',
invalidName: 'Invalid Cluster!',
};

// Helper functions
async function navigateToSettings(page: Page) {
await page.waitForLoadState('load');
await page.getByRole('button', { name: 'Settings' }).click();
await page.waitForLoadState('load');
}

async function verifyClusterName(page: Page, expectedName: string) {
await navigateToSettings(page);
await expect(page.locator('h2')).toContainText(`Cluster Settings (${expectedName})`);
}

async function renameCluster(
page: Page,
fromName: string,
toName: string,
confirm: boolean = true
) {
await page.fill(`input[placeholder="${fromName}"]`, toName);
await page.getByRole('button', { name: 'Apply' }).click();
await page.getByRole('button', { name: confirm ? 'Yes' : 'No' }).click();
}

// Setup
test.beforeAll(async () => {
electronApp = await _electron.launch({
cwd: appPath,
executablePath: electronPath,
args: ['.'],
env: {
...process.env,
NODE_ENV: 'development',
ELECTRON_DEV: 'true',
},
});

electronPage = await electronApp.firstWindow();
});

test.beforeEach(async ({ page }) => {
page.close();
});

// Tests
test.describe('Cluster rename functionality', () => {
test.beforeEach(() => {
test.skip(process.env.PLAYWRIGHT_TEST_MODE !== 'app', 'These tests only run in app mode');
});

test('should rename cluster and verify changes', async ({ page: browserPage }) => {
const page = process.env.PLAYWRIGHT_TEST_MODE === 'app' ? electronPage : browserPage;
const headlampPage = new HeadlampPage(page);
await headlampPage.authenticate();

await navigateToSettings(page);
await expect(page.locator('h2')).toContainText('Cluster Settings');

// Test invalid inputs
await page.fill('input[placeholder="minikube"]', TEST_CONFIG.invalidName);
await expect(page.getByRole('button', { name: 'Apply' })).toBeDisabled();

await page.fill('input[placeholder="minikube"]', '');
await expect(page.getByRole('button', { name: 'Apply' })).toBeDisabled();

// Test successful rename
await renameCluster(page, TEST_CONFIG.originalName, TEST_CONFIG.newName);
await verifyClusterName(page, TEST_CONFIG.newName);

// Rename back to original
await renameCluster(page, TEST_CONFIG.newName, TEST_CONFIG.originalName);
await verifyClusterName(page, TEST_CONFIG.originalName);
});

test('should cancel rename operation', async ({ page: browserPage }) => {
const page = process.env.PLAYWRIGHT_TEST_MODE === 'app' ? electronPage : browserPage;
const headlampPage = new HeadlampPage(page);
await headlampPage.authenticate();

await navigateToSettings(page);
await renameCluster(page, TEST_CONFIG.originalName, TEST_CONFIG.newName, false);
await expect(page.getByText(`Cluster Settings (${TEST_CONFIG.originalName})`)).toBeVisible();
});
});
39 changes: 39 additions & 0 deletions frontend/src/components/App/Settings/ClusterNameEditor.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Meta, StoryObj } from '@storybook/react';
import { ClusterNameEditor } from './ClusterNameEditor';

const meta: Meta<typeof ClusterNameEditor> = {
title: 'Settings/ClusterNameEditor',
component: ClusterNameEditor,
parameters: {
layout: 'centered',
},
};

export default meta;
type Story = StoryObj<typeof ClusterNameEditor>;

export const Default: Story = {
args: {
cluster: 'my-cluster',
newClusterName: '',
isValidCurrentName: true,
source: 'dynamic_cluster',
onClusterNameChange: () => {},
onUpdateClusterName: () => {},
},
};

export const WithInvalidName: Story = {
args: {
...Default.args,
newClusterName: 'Invalid Cluster Name',
isValidCurrentName: false,
},
};

export const WithNewName: Story = {
args: {
...Default.args,
newClusterName: 'new-cluster-name',
},
};
88 changes: 88 additions & 0 deletions frontend/src/components/App/Settings/ClusterNameEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Box, TextField } from '@mui/material';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ConfirmButton, NameValueTable } from '../../common';

interface ClusterNameEditorProps {
cluster: string;
newClusterName: string;
isValidCurrentName: boolean;
source: string;
onClusterNameChange: (name: string) => void;
onUpdateClusterName: (source: string) => void;
}

export function ClusterNameEditor({
cluster,
newClusterName,
isValidCurrentName,
source,
onClusterNameChange,
onUpdateClusterName,
}: ClusterNameEditorProps) {
const { t } = useTranslation(['translation']);

const invalidClusterNameMessage = t(
"translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character."
);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.replace(' ', '');
onClusterNameChange(value);
};

const handleKeyPress = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' && isValidCurrentName) {
onUpdateClusterName(source);
}
};

return (
<NameValueTable
rows={[
{
name: t('translation|Name'),
value: (
<TextField
onChange={handleChange}
value={newClusterName}
placeholder={cluster}
error={!isValidCurrentName}
helperText={
isValidCurrentName
? t(
'translation|The current name of cluster. You can define custom modified name.'
)
: invalidClusterNameMessage
}
InputProps={{
endAdornment: (
<Box pt={2} textAlign="right">
<ConfirmButton
onConfirm={() => {
if (isValidCurrentName) {
onUpdateClusterName(source);
}
}}
confirmTitle={t('translation|Change name')}
confirmDescription={t(
'translation|Are you sure you want to change the name for "{{ clusterName }}"?',
{ clusterName: cluster }
)}
disabled={!newClusterName || !isValidCurrentName}
>
{t('translation|Apply')}
</ConfirmButton>
</Box>
),
onKeyPress: handleKeyPress,
autoComplete: 'off',
sx: { maxWidth: 250 },
}}
/>
),
},
]}
/>
);
}
65 changes: 8 additions & 57 deletions frontend/src/components/App/Settings/SettingsCluster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { findKubeconfigByClusterName, updateStatelessClusterKubeconfig } from '.
import { Link, Loader, NameValueTable, SectionBox } from '../../common';
import ConfirmButton from '../../common/ConfirmButton';
import Empty from '../../common/EmptyContent';
import { ClusterNameEditor } from './ClusterNameEditor';

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

const invalidClusterNameMessage = t(
"translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character."
);

// If we don't have yet a cluster name from the URL, we are still loading.
if (!clusterFromURLRef.current) {
return <Loader title="Loading" />;
Expand Down Expand Up @@ -333,59 +330,13 @@ export default function SettingsCluster() {
</Link>
</Box>
{helpers.isElectron() && (
<NameValueTable
rows={[
{
name: t('translation|Name'),
value: (
<TextField
onChange={event => {
let value = event.target.value;
value = value.replace(' ', '');
setNewClusterName(value);
}}
value={newClusterName}
placeholder={cluster}
error={!isValidCurrentName}
helperText={
isValidCurrentName
? t(
'translation|The current name of cluster. You can define custom modified name.'
)
: invalidClusterNameMessage
}
InputProps={{
endAdornment: (
<Box pt={2} textAlign="right">
<ConfirmButton
onConfirm={() => {
if (isValidCurrentName) {
handleUpdateClusterName(source);
}
}}
confirmTitle={t('translation|Change name')}
confirmDescription={t(
'translation|Are you sure you want to change the name for "{{ clusterName }}"?',
{ clusterName: cluster }
)}
disabled={!newClusterName || !isValidCurrentName}
>
{t('translation|Apply')}
</ConfirmButton>
</Box>
),
onKeyPress: event => {
if (event.key === 'Enter' && isValidCurrentName) {
handleUpdateClusterName(source);
}
},
autoComplete: 'off',
sx: { maxWidth: 250 },
}}
/>
),
},
]}
<ClusterNameEditor
cluster={cluster}
newClusterName={newClusterName}
isValidCurrentName={isValidCurrentName}
source={source}
onClusterNameChange={setNewClusterName}
onUpdateClusterName={handleUpdateClusterName}
/>
)}
<NameValueTable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<body>
<div>
<dl
class="MuiGrid-root MuiGrid-container css-uximdg-MuiGrid-root"
>
<dt
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-4 css-16fae59-MuiGrid-root"
>
Name
</dt>
<dd
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-8 css-1xrovmc-MuiGrid-root"
>
<div
class="MuiFormControl-root MuiTextField-root css-1u3bzj6-MuiFormControl-root-MuiTextField-root"
>
<div
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-colorPrimary MuiInputBase-formControl MuiInputBase-adornedEnd css-12pocct-MuiInputBase-root-MuiInput-root"
>
<input
aria-describedby=":r15:-helper-text"
aria-invalid="false"
autocomplete="off"
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd css-1x51dt5-MuiInputBase-input-MuiInput-input"
id=":mock-test-id:"
placeholder="my-cluster"
type="text"
value=""
/>
<div
class="MuiBox-root css-oxzk7u"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary Mui-disabled MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary css-1gp6czg-MuiButtonBase-root-MuiButton-root"
disabled=""
tabindex="-1"
type="button"
>
Apply
</button>
<div />
</div>
</div>
<p
class="MuiFormHelperText-root MuiFormHelperText-sizeMedium css-nnd8o8-MuiFormHelperText-root"
id=":mock-test-id:"
>
The current name of cluster. You can define custom modified name.
</p>
</div>
</dd>
</dl>
</div>
</body>
Loading
Loading