From a57ff2d1431d961808c2bbfdf0997abe25ab0e34 Mon Sep 17 00:00:00 2001 From: Mia Wong Date: Wed, 23 Oct 2024 15:04:02 -0400 Subject: [PATCH] Unboxing the embedded cluster (#4951) Co-authored-by: Salah Al Saleh --- e2e/playwright/tests/@smoke-test/test.spec.ts | 496 ++++++---- kurl_proxy/.gitignore | 1 + kurl_proxy/assets/images/check.svg | 4 + kurl_proxy/assets/images/down_arrow.svg | 3 + kurl_proxy/assets/images/up_arrow.svg | 3 + kurl_proxy/assets/input.css | 3 + kurl_proxy/assets/insecure.html | 274 ++++-- kurl_proxy/assets/output.css | 792 +++++++++++++++ kurl_proxy/assets/tls-custom.css | 328 +++++-- kurl_proxy/assets/tls.html | 262 +++-- kurl_proxy/assets/tls.js | 74 +- kurl_proxy/assets/welcome.html | 100 ++ kurl_proxy/cmd/main.go | 15 +- kurl_proxy/tailwind.config.js | 8 + web/src/Root.tsx | 917 +++++++++++------- web/src/components/PreflightResultPage.tsx | 28 +- .../apps/EmbeddedClusterManagement.tsx | 78 +- .../components/identity/ConfigureIngress.jsx | 2 +- .../components/modals/BackupRestoreModal.jsx | 10 +- .../modals/RestoreSnapshotModal.jsx | 2 +- web/src/components/selection.json | 47 + .../snapshots/AppSnapshotRestore.jsx | 2 +- .../components/snapshots/SnapshotRestore.jsx | 2 +- .../components/snapshots/SnapshotSchedule.jsx | 2 +- web/src/components/snapshots/Snapshots.jsx | 2 +- .../GenerateSupportBundleModal.tsx | 4 +- .../upgrade_service/PreflightChecks.tsx | 4 +- .../AddNewApp/components/InstallWithHelm.tsx | 2 +- .../api/getAdminConsoleUpdateStatus.tsx | 4 +- .../api/postUpdateAdminConsole.tsx | 8 +- .../AppConfig/components/AppConfig.tsx | 172 ++-- .../api/getDownloadAppVersionStatus.tsx | 2 +- .../Auth/components/SecureAdminConsole.tsx | 8 +- .../components/DashboardGitOpsCard.jsx | 2 +- .../features/Gitops/components/AppGitops.jsx | 2 +- .../Gitops/components/SetupProvider.jsx | 2 +- .../Gitops/components/modals/DisableModal.jsx | 2 +- .../scss/components/watches/WatchConfig.scss | 4 +- 38 files changed, 2760 insertions(+), 911 deletions(-) create mode 100644 kurl_proxy/assets/images/check.svg create mode 100644 kurl_proxy/assets/images/down_arrow.svg create mode 100644 kurl_proxy/assets/images/up_arrow.svg create mode 100644 kurl_proxy/assets/input.css create mode 100644 kurl_proxy/assets/output.css create mode 100644 kurl_proxy/assets/welcome.html create mode 100644 kurl_proxy/tailwind.config.js diff --git a/e2e/playwright/tests/@smoke-test/test.spec.ts b/e2e/playwright/tests/@smoke-test/test.spec.ts index c9d375b5cd..214517f7e0 100644 --- a/e2e/playwright/tests/@smoke-test/test.spec.ts +++ b/e2e/playwright/tests/@smoke-test/test.spec.ts @@ -1,213 +1,347 @@ -import { test, expect } from '@playwright/test'; -import { login, uploadLicense } from '../shared'; +import { test, expect } from "@playwright/test"; +import { login, uploadLicense } from "../shared"; const { execSync } = require("child_process"); -test('smoke test', async ({ page }) => { +test("smoke test", async ({ page }) => { test.setTimeout(5 * 60 * 1000); // 5 minutes await login(page); await uploadLicense(page, expect); - await expect(page.locator('#app')).toContainText('Install in airgapped environment', { timeout: 15000 }); - await page.getByText('download App Name from the Internet').click(); - await expect(page.locator('#app')).toContainText('Installing your license'); - await expect(page.locator('h3')).toContainText('My Example Config', { timeout: 30000 }); - await page.locator('#a_bool-group').getByText('a bool field').click(); - await page.locator('#a_required_text-group').getByRole('textbox').click(); - await page.locator('#a_required_text-group').getByRole('textbox').fill('my required text field'); - await expect(page.locator('#version_sequence-group')).toContainText('This version is 0'); - await page.getByRole('button', { name: 'Continue' }).click(); - await expect(page.locator('#app')).toContainText('Results', { timeout: 30000 }); - await expect(page.locator('#app')).toContainText('Sequence is 0'); - await page.getByRole('button', { name: 'Deploy' }).click(); - await page.getByRole('button', { name: 'Deploy anyway' }).click(); - await expect(page.locator('#app')).toContainText('Ready', { timeout: 30000 }); - await expect(page.locator('#app')).toContainText('Currently deployed version', { timeout: 30000 }); - await expect(page.locator('#app')).toContainText('Check for update'); - await expect(page.locator('#app')).toContainText('Redeploy', { timeout: 15000 }); - await expect(page.getByText('App Name')).toBeVisible(); - await expect(page.locator('.Dashboard--appIcon')).toBeVisible(); - await expect(page.locator('p').filter({ hasText: 'License' })).toBeVisible(); - await page.getByText('Configure automatic updates').click(); - await expect(page.locator('.ConfigureUpdatesModal')).toContainText('Default'); - await expect(page.locator('.ConfigureUpdatesModal')).toContainText('Every 4 hours'); - await expect(page.locator('label')).toContainText('Enable automatic deployment'); - await page.locator('.replicated-select__control').click(); + await expect(page.locator("#app")).toContainText( + "Install in airgapped environment", + { timeout: 15000 }, + ); + await page.getByText("download App Name from the Internet").click(); + await expect(page.locator("#app")).toContainText("Installing your license"); + await expect(page.locator("h3")).toContainText("My Example Config", { + timeout: 30000, + }); + await page.locator("#a_bool-group").getByText("a bool field").click(); + await page.locator("#a_required_text-group").getByRole("textbox").click(); + await page + .locator("#a_required_text-group") + .getByRole("textbox") + .fill("my required text field"); + await expect(page.locator("#version_sequence-group")).toContainText( + "This version is 0", + ); + await page.getByRole("button", { name: "Continue" }).click(); + await expect(page.locator("#app")).toContainText("Results", { + timeout: 30000, + }); + await expect(page.locator("#app")).toContainText("Sequence is 0"); + await page.getByRole("button", { name: "Deploy" }).click(); + await page.getByRole("button", { name: "Deploy anyway" }).click(); + await expect(page.locator("#app")).toContainText("Ready", { timeout: 30000 }); + await expect(page.locator("#app")).toContainText( + "Currently deployed version", + { timeout: 30000 }, + ); + await expect(page.locator("#app")).toContainText("Check for update"); + await expect(page.locator("#app")).toContainText("Redeploy", { + timeout: 15000, + }); + await expect(page.getByText("App Name")).toBeVisible(); + await expect(page.locator(".Dashboard--appIcon")).toBeVisible(); + await expect(page.locator("p").filter({ hasText: "License" })).toBeVisible(); + await page.getByText("Configure automatic updates").click(); + await expect(page.locator(".ConfigureUpdatesModal")).toContainText("Default"); + await expect(page.locator(".ConfigureUpdatesModal")).toContainText( + "Every 4 hours", + ); + await expect(page.locator("label")).toContainText( + "Enable automatic deployment", + ); + await page.locator(".replicated-select__control").click(); await page.waitForTimeout(1000); - await page.locator('.replicated-select__option').getByText('Weekly', { exact: true }).click(); + await page + .locator(".replicated-select__option") + .getByText("Weekly", { exact: true }) + .click(); await page.waitForTimeout(1000); - await expect(page.locator('.ConfigureUpdatesModal')).toContainText('Weekly'); - await expect(page.locator('.ConfigureUpdatesModal')).toContainText('At 12:00 AM, only on Sunday'); - await page.getByRole('button', { name: 'Update', exact: true }).click(); - await expect(page.getByText('Automatically check for updates', { exact: true })).not.toBeVisible(); - await page.locator('svg.icons.clickable[data-tip="View release notes"]').click(); - await expect(page.getByLabel('Release Notes').getByRole('paragraph')).toContainText('release notes - updates'); - await page.getByRole('button', { name: 'Close' }).click(); + await expect(page.locator(".ConfigureUpdatesModal")).toContainText("Weekly"); + await expect(page.locator(".ConfigureUpdatesModal")).toContainText( + "At 12:00 AM, only on Sunday", + ); + await page.getByRole("button", { name: "Update", exact: true }).click(); + await expect( + page.getByText("Automatically check for updates", { exact: true }), + ).not.toBeVisible(); + await page + .locator('svg.icons.clickable[data-tip="View release notes"]') + .click(); + await expect( + page.getByLabel("Release Notes").getByRole("paragraph"), + ).toContainText("release notes - updates"); + await page.getByRole("button", { name: "Close" }).click(); await page.locator('span[data-tip="View deploy logs"]').click(); await validateDeployLogs(page, expect); - await page.getByRole('link', { name: 'Version history' }).click(); - await expect(page.locator('.currentVersion--wrapper')).toContainText('Sequence 0'); - await expect(page.locator('#app')).toContainText('Currently deployed version'); - await expect(page.locator('#app')).toContainText('Check for update'); - await expect(page.locator('#app')).toContainText('Configure automatic updates'); - await expect(page.getByRole('button')).toContainText('Redeploy'); - await page.getByText('Configure automatic updates').click(); - await expect(page.locator('.ConfigureUpdatesModal')).toContainText('Weekly'); - await expect(page.locator('.ConfigureUpdatesModal')).toContainText('At 12:00 AM, only on Sunday'); - await expect(page.locator('label')).toContainText('Enable automatic deployment'); - await page.getByRole('button', { name: 'Cancel' }).click(); - await expect(page.getByText('Automatically check for updates', { exact: true })).not.toBeVisible(); + await page.getByRole("link", { name: "Version history" }).click(); + await expect(page.locator(".currentVersion--wrapper")).toContainText( + "Sequence 0", + ); + await expect(page.locator("#app")).toContainText( + "Currently deployed version", + ); + await expect(page.locator("#app")).toContainText("Check for update"); + await expect(page.locator("#app")).toContainText( + "Configure automatic updates", + ); + await expect(page.getByRole("button")).toContainText("Redeploy"); + await page.getByText("Configure automatic updates").click(); + await expect(page.locator(".ConfigureUpdatesModal")).toContainText("Weekly"); + await expect(page.locator(".ConfigureUpdatesModal")).toContainText( + "At 12:00 AM, only on Sunday", + ); + await expect(page.locator("label")).toContainText( + "Enable automatic deployment", + ); + await page.getByRole("button", { name: "Cancel" }).click(); + await expect( + page.getByText("Automatically check for updates", { exact: true }), + ).not.toBeVisible(); await page.locator('span[data-tip="View deploy logs"]').first().click(); await validateDeployLogs(page, expect); - await page.getByRole('link', { name: 'Config', exact: true }).click(); - await expect(page.locator('h3')).toContainText('My Example Config'); - await expect(page.locator('#version_sequence-group')).toContainText('This version is 1'); + await page.getByRole("link", { name: "Config", exact: true }).click(); + await expect(page.locator("h3")).toContainText("My Example Config"); + await expect(page.locator("#version_sequence-group")).toContainText( + "This version is 1", + ); await expect(page.getByRole("combobox")).toHaveValue("option_1"); await page.getByRole("combobox").selectOption("option_2"); await expect(page.getByRole("combobox")).toHaveValue("option_2"); await expect(page.getByLabel("radio_1")).toBeChecked(); await page.getByLabel("radio_2").click(); await expect(page.getByLabel("radio_2")).toBeChecked(); - await expect(page.getByRole('button', { name: 'Save config' })).toBeVisible(); - await page.getByRole('link', { name: 'Troubleshoot' }).click(); - await expect(page.getByRole('button', { name: 'Analyze App Name' })).toBeVisible(); - await page.getByRole('link', { name: 'License' }).click(); - await expect(page.locator('#app')).toContainText('Airgap enabled'); - await expect(page.locator('#app')).toContainText('Snapshots enabled'); - await expect(page.getByRole('button', { name: 'Sync license' })).toBeVisible(); - await page.getByRole('link', { name: 'View files' }).click(); - await page.getByText('upstream', { exact: true }).click(); - await page.getByRole('listitem', { name: 'config.yaml' }).locator('div').click(); - await expect(page.locator('.view-lines')).toContainText('apiVersion'); - await page.getByText('Click here', { exact: true }).click(); - await expect(page.getByRole('heading')).toContainText('Edit patches for your kots application'); - await expect(page.getByText('Copy command').first()).toBeVisible(); + await expect(page.getByRole("button", { name: "Save config" })).toBeVisible(); + await page.getByRole("link", { name: "Troubleshoot" }).click(); + await expect( + page.getByRole("button", { name: "Analyze App Name" }), + ).toBeVisible(); + await page.getByRole("link", { name: "License" }).click(); + await expect(page.locator("#app")).toContainText("Airgap enabled"); + await expect(page.locator("#app")).toContainText("Snapshots enabled"); + await expect( + page.getByRole("button", { name: "Sync license" }), + ).toBeVisible(); + await page.getByRole("link", { name: "View files" }).click(); + await page.getByText("upstream", { exact: true }).click(); + await page + .getByRole("listitem", { name: "config.yaml" }) + .locator("div") + .click(); + await expect(page.locator(".view-lines")).toContainText("apiVersion"); + await page.getByText("Click here", { exact: true }).click(); + await expect(page.getByRole("heading")).toContainText( + "Edit patches for your kots application", + ); + await expect(page.getByText("Copy command").first()).toBeVisible(); - let downloadCommand = await page.locator('.react-prism.language-bash').first().textContent(); - if (!downloadCommand!.includes('download')) { - throw new Error("Expected the download command to contain the word 'download'"); + let downloadCommand = await page + .locator(".react-prism.language-bash") + .first() + .textContent(); + if (!downloadCommand!.includes("download")) { + throw new Error( + "Expected the download command to contain the word 'download'", + ); } downloadCommand = `${downloadCommand} --overwrite`; console.log(downloadCommand, "\n"); - execSync(downloadCommand, {stdio: 'inherit'}); + execSync(downloadCommand, { stdio: "inherit" }); - await expect(page.getByText('Copy command').last()).toBeVisible(); - let uploadCommand = await page.locator('.react-prism.language-bash').last().textContent(); - if (!uploadCommand!.includes('upload')) { + await expect(page.getByText("Copy command").last()).toBeVisible(); + let uploadCommand = await page + .locator(".react-prism.language-bash") + .last() + .textContent(); + if (!uploadCommand!.includes("upload")) { throw new Error("Expected the upload command to contain the word 'upload'"); } console.log(uploadCommand, "\n"); - execSync(uploadCommand, {stdio: 'inherit'}); + execSync(uploadCommand, { stdio: "inherit" }); - await page.getByRole('button', { name: 'Ok, got it!' }).click(); - await page.getByRole('link', { name: 'Version history' }).click(); - await expect(page.locator('#app')).toContainText('KOTS Upload'); - await expect(page.getByText('Running checks', { exact: true }).first()).not.toBeVisible({ timeout: 30000 }); - await page.getByRole('button', { name: 'Deploy' }).first().click(); - await page.getByRole('button', { name: 'Deploy this version' }).click(); - await expect(page.locator('#app')).toContainText('Deploying'); - await expect(page.locator('#app')).toContainText('Currently deployed version'); - await expect(page.locator('#app')).toContainText('Application up to date.'); - await expect(page.locator('.currentVersion--wrapper')).toContainText('Sequence 1'); - await page.getByRole('link', { name: 'Registry settings' }).click(); - await page.getByPlaceholder('artifactory.some-big-bank.com').click(); - await page.getByPlaceholder('artifactory.some-big-bank.com').fill('ttl.sh'); - await page.getByPlaceholder('username').click(); - await page.getByPlaceholder('username').fill('admin'); - await page.getByPlaceholder('password').click(); - await page.getByPlaceholder('password').fill('admin'); - await page.getByRole('button', { name: 'Test connection' }).click(); - await expect(page.locator('form')).toContainText('Success!'); - await page.getByRole('button', { name: 'Save changes' }).click(); - await expect(page.getByRole('button', { name: 'Save changes' })).toBeDisabled(); - await expect(page.locator('.Loader')).toBeVisible(); - await expect(page.locator('#app')).toContainText('Writing manifest to image destination', { timeout: 30000 }); - await expect(page.getByRole('button', { name: 'Save changes' })).toBeEnabled({ timeout: 60000 }); - await expect(page.locator('.Loader')).not.toBeVisible(); - await page.getByRole('link', { name: 'Version history' }).click(); - await expect(page.locator('#app')).toContainText('Registry Change'); - await expect(page.locator('#app')).toContainText('Sequence 2'); - await page.getByRole('link', { name: 'Registry settings' }).click(); - await page.getByRole('button', { name: 'Stop using registry' }).click(); - await page.getByRole('button', { name: 'OK' }).click(); - await expect(page.locator('.Loader')).toBeVisible(); - await expect(page.getByRole('button', { name: 'Stop using registry' })).toBeDisabled(); - await expect(page.getByRole('button', { name: 'Save changes' })).toBeEnabled({ timeout: 30000 }); - await expect(page.locator('.Loader')).not.toBeVisible(); - await expect(page.getByPlaceholder('artifactory.some-big-bank.com')).toBeEmpty(); - await expect(page.getByPlaceholder('username')).toBeEmpty(); - await expect(page.getByPlaceholder('password')).toBeEmpty(); - await expect(page.getByPlaceholder('namespace')).toBeEmpty(); + await page.getByRole("button", { name: "Ok, got it!" }).click(); + await page.getByRole("link", { name: "Version history" }).click(); + await expect(page.locator("#app")).toContainText("KOTS Upload"); + await expect( + page.getByText("Running checks", { exact: true }).first(), + ).not.toBeVisible({ timeout: 30000 }); + await page.getByRole("button", { name: "Deploy" }).first().click(); + await page.getByRole("button", { name: "Deploy this version" }).click(); + await expect(page.locator("#app")).toContainText("Deploying"); + await expect(page.locator("#app")).toContainText( + "Currently deployed version", + ); + await expect(page.locator("#app")).toContainText("Application up to date."); + await expect(page.locator(".currentVersion--wrapper")).toContainText( + "Sequence 1", + ); + await page.getByRole("link", { name: "Registry settings" }).click(); + await page.getByPlaceholder("artifactory.some-big-bank.com").click(); + await page.getByPlaceholder("artifactory.some-big-bank.com").fill("ttl.sh"); + await page.getByPlaceholder("username").click(); + await page.getByPlaceholder("username").fill("admin"); + await page.getByPlaceholder("password").click(); + await page.getByPlaceholder("password").fill("admin"); + await page.getByRole("button", { name: "Test connection" }).click(); + await expect(page.locator("form")).toContainText("Success!"); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect( + page.getByRole("button", { name: "Save changes" }), + ).toBeDisabled(); + await expect(page.locator(".Loader")).toBeVisible(); + await expect(page.locator("#app")).toContainText( + "Writing manifest to image destination", + { timeout: 30000 }, + ); + await expect(page.getByRole("button", { name: "Save changes" })).toBeEnabled({ + timeout: 60000, + }); + await expect(page.locator(".Loader")).not.toBeVisible(); + await page.getByRole("link", { name: "Version history" }).click(); + await expect(page.locator("#app")).toContainText("Registry Change"); + await expect(page.locator("#app")).toContainText("Sequence 2"); + await page.getByRole("link", { name: "Registry settings" }).click(); + await page.getByRole("button", { name: "Stop using registry" }).click(); + await page.getByRole("button", { name: "OK" }).click(); + await expect(page.locator(".Loader")).toBeVisible(); + await expect( + page.getByRole("button", { name: "Stop using registry" }), + ).toBeDisabled(); + await expect(page.getByRole("button", { name: "Save changes" })).toBeEnabled({ + timeout: 30000, + }); + await expect(page.locator(".Loader")).not.toBeVisible(); + await expect( + page.getByPlaceholder("artifactory.some-big-bank.com"), + ).toBeEmpty(); + await expect(page.getByPlaceholder("username")).toBeEmpty(); + await expect(page.getByPlaceholder("password")).toBeEmpty(); + await expect(page.getByPlaceholder("namespace")).toBeEmpty(); await page.waitForTimeout(2000); - await page.getByRole('link', { name: 'Version history' }).click(); - await expect(page.locator('#app')).toContainText('Sequence 3', { timeout: 10000 }); - await expect(page.locator('#app')).toContainText('Registry Change'); - await expect(page.locator('.NavItem').getByText('Application', { exact: true })).toBeVisible(); - await expect(page.locator('.NavItem').getByText('GitOps', { exact: true })).toBeVisible(); - await expect(page.locator('.NavItem').getByText('Snapshots', { exact: true })).toBeVisible(); - await expect(page.locator('div').filter({ hasText: /^Change passwordAdd new applicationLog out$/ }).getByRole('img')).toBeVisible(); - await page.locator('.NavItem').getByText('Snapshots', { exact: true }).click(); - await page.getByRole('link', { name: 'Settings & Schedule' }).click(); - await expect(page.locator('#app')).toContainText('Snapshot settings'); - await page.getByText('+ Add a new destination').click(); - await expect(page.getByRole('button', { name: 'Check for Velero' })).toBeVisible(); - await page.getByRole('button', { name: 'Check for Velero' }).click(); - await expect(page.getByLabel('Modal')).toContainText('Velero is installed on your cluster'); - await page.getByRole('button', { name: 'Ok, got it!' }).click(); - await page.getByRole('link', { name: 'Full Snapshots (Instance)' }).click(); - await expect(page.locator('#app')).toContainText('No snapshots yet'); - await page.getByRole('button', { name: 'Start a snapshot' }).click(); - await expect(page.locator('#app')).toContainText('In Progress'); - await expect(page.locator('#app')).toContainText('Completed', { timeout: 300000 }); - await page.getByText('Learn more').click(); - await page.getByRole('button', { name: 'Ok, got it!' }).click(); - await expect(page.locator('#app')).toContainText('Full Snapshots (Instance)'); - await page.getByRole('link', { name: 'Partial Snapshots (Application)' }).click(); - await page.getByRole('button', { name: 'Start a snapshot' }).click(); - await expect(page.locator('#app')).toContainText('Completed', { timeout: 30000 }); - await expect(page.getByText('It’s recommend that you use')).toBeVisible(); - await page.getByText('Learn more').click(); - await page.getByRole('button', { name: 'Ok, got it!' }).click(); - await expect(page.locator('#app')).toContainText('Partial snapshots (Application)'); - await page.getByRole('link', { name: 'Full Snapshots (Instance)', exact: true }).click(); - await page.locator('.SnapshotRow--wrapper').click(); - await expect(page.locator('#app')).toContainText('Snapshot timeline'); - await page.getByText('View logs').click(); - await expect(page.locator('.view-lines')).toContainText('level=info'); - await page.getByRole('button', { name: 'Ok, got it!' }).click(); - await page.getByRole('link', { name: 'Full Snapshots (Instance)', exact: true }).click(); - await page.locator('svg.icons.clickable[data-tip="Restore from this backup"]').click(); - await expect(page.getByLabel('Modal')).toContainText('Restore from backup'); - await expect(page.getByLabel('Modal')).toContainText('Admin console & application'); - await expect(page.getByLabel('Modal')).toContainText('Application & metadata only'); - await expect(page.getByLabel('Modal')).toContainText('Only restores the admin console'); - await page.getByText('Application & metadata only', { exact: true }).click(); - await page.getByRole('button', { name: 'Cancel' }).click(); - await page.locator('svg.icons.clickable').last().click(); - await expect(page.getByLabel('Modal')).toContainText('Delete snapshot'); - await page.getByRole('button', { name: 'Delete snapshot' }).click(); - await expect(page.locator('#app')).toContainText('Deleting'); - await expect(page.locator('#app')).toContainText('No snapshots yet', { timeout: 30000 }); - await page.getByRole('link', { name: 'Settings & Schedule' }).click(); - await page.getByRole('button', { name: 'Update storage settings' }).click(); - await expect(page.locator('form')).toContainText('Settings updated', { timeout: 30000 }); - await page.getByRole('button', { name: 'Update schedule' }).click(); - await expect(page.locator('#app')).toContainText('Schedule updated'); - await page.locator('div').filter({ hasText: /^Change passwordAdd new applicationLog out$/ }).getByRole('img').click(); - await page.getByText('Log out', { exact: true }).click(); - await expect(page.getByPlaceholder('password')).toBeVisible({ timeout: 30000 }); - await expect(page.locator('#app')).toContainText('Enter the password to access the App Name admin console.'); - await expect(page.getByRole('button')).toContainText('Log in'); + await page.getByRole("link", { name: "Version history" }).click(); + await expect(page.locator("#app")).toContainText("Sequence 3", { + timeout: 10000, + }); + await expect(page.locator("#app")).toContainText("Registry Change"); + await expect( + page.locator(".NavItem").getByText("Application", { exact: true }), + ).toBeVisible(); + await expect( + page.locator(".NavItem").getByText("GitOps", { exact: true }), + ).toBeVisible(); + await expect( + page.locator(".NavItem").getByText("Snapshots", { exact: true }), + ).toBeVisible(); + await expect( + page + .locator("div") + .filter({ hasText: /^Change passwordAdd new applicationLog out$/ }) + .getByRole("img"), + ).toBeVisible(); + await page + .locator(".NavItem") + .getByText("Snapshots", { exact: true }) + .click(); + await page.getByRole("link", { name: "Settings & Schedule" }).click(); + await expect(page.locator("#app")).toContainText("Snapshot settings"); + await page.getByText("+ Add a new destination").click(); + await expect( + page.getByRole("button", { name: "Check for Velero" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Check for Velero" }).click(); + await expect(page.getByLabel("Modal")).toContainText( + "Velero is installed on your cluster", + ); + await page.getByRole("button", { name: "Ok, got it!" }).click(); + await page.getByRole("link", { name: "Full Snapshots (Instance)" }).click(); + await expect(page.locator("#app")).toContainText("No snapshots yet"); + await page.getByRole("button", { name: "Start a snapshot" }).click(); + await expect(page.locator("#app")).toContainText("In Progress"); + await expect(page.locator("#app")).toContainText("Completed", { + timeout: 300000, + }); + await page.getByText("Learn more").click(); + await page.getByRole("button", { name: "Ok, got it!" }).click(); + await expect(page.locator("#app")).toContainText("Full Snapshots (Instance)"); + await page + .getByRole("link", { name: "Partial Snapshots (Application)" }) + .click(); + await page.getByRole("button", { name: "Start a snapshot" }).click(); + await expect(page.locator("#app")).toContainText("Completed", { + timeout: 30000, + }); + await expect(page.getByText("It’s recommend that you use")).toBeVisible(); + await page.getByText("Learn more").click(); + await page.getByRole("button", { name: "Ok, got it!" }).click(); + await expect(page.locator("#app")).toContainText( + "Partial snapshots (Application)", + ); + await page + .getByRole("link", { name: "Full Snapshots (Instance)", exact: true }) + .click(); + await page.locator(".SnapshotRow--wrapper").click(); + await expect(page.locator("#app")).toContainText("Snapshot timeline"); + await page.getByText("View logs").click(); + await expect(page.locator(".view-lines")).toContainText("level=info"); + await page.getByRole("button", { name: "Ok, got it!" }).click(); + await page + .getByRole("link", { name: "Full Snapshots (Instance)", exact: true }) + .click(); + await page + .locator('svg.icons.clickable[data-tip="Restore from this backup"]') + .click(); + await expect(page.getByLabel("Modal")).toContainText("Restore from backup"); + await expect(page.getByLabel("Modal")).toContainText( + "Admin Console & application", + ); + await expect(page.getByLabel("Modal")).toContainText( + "Application & metadata only", + ); + await expect(page.getByLabel("Modal")).toContainText( + "Only restores the Admin Console", + ); + await page.getByText("Application & metadata only", { exact: true }).click(); + await page.getByRole("button", { name: "Cancel" }).click(); + await page.locator("svg.icons.clickable").last().click(); + await expect(page.getByLabel("Modal")).toContainText("Delete snapshot"); + await page.getByRole("button", { name: "Delete snapshot" }).click(); + await expect(page.locator("#app")).toContainText("Deleting"); + await expect(page.locator("#app")).toContainText("No snapshots yet", { + timeout: 30000, + }); + await page.getByRole("link", { name: "Settings & Schedule" }).click(); + await page.getByRole("button", { name: "Update storage settings" }).click(); + await expect(page.locator("form")).toContainText("Settings updated", { + timeout: 30000, + }); + await page.getByRole("button", { name: "Update schedule" }).click(); + await expect(page.locator("#app")).toContainText("Schedule updated"); + await page + .locator("div") + .filter({ hasText: /^Change passwordAdd new applicationLog out$/ }) + .getByRole("img") + .click(); + await page.getByText("Log out", { exact: true }).click(); + await expect(page.getByPlaceholder("password")).toBeVisible({ + timeout: 30000, + }); + await expect(page.locator("#app")).toContainText( + "Enter the password to access the App Name Admin Console.", + ); + await expect(page.getByRole("button")).toContainText("Log in"); }); const validateDeployLogs = async (page, expect) => { - await expect(page.getByText('dryrunStdout')).toBeVisible(); - await expect(page.getByText('dryrunStderr')).toBeVisible(); - await expect(page.getByText('applyStdout')).toBeVisible(); - await expect(page.getByText('applyStderr')).toBeVisible(); - await expect(page.getByText('helmStdout')).toBeVisible(); - await expect(page.getByText('helmStderr')).toBeVisible(); - await page.getByText('dryrunStderr').click(); - await page.getByText('applyStdout').click(); - await expect(page.locator('.view-lines')).toContainText('created'); - await page.getByRole('button', { name: 'Ok, got it!' }).click(); + await expect(page.getByText("dryrunStdout")).toBeVisible(); + await expect(page.getByText("dryrunStderr")).toBeVisible(); + await expect(page.getByText("applyStdout")).toBeVisible(); + await expect(page.getByText("applyStderr")).toBeVisible(); + await expect(page.getByText("helmStdout")).toBeVisible(); + await expect(page.getByText("helmStderr")).toBeVisible(); + await page.getByText("dryrunStderr").click(); + await page.getByText("applyStdout").click(); + await expect(page.locator(".view-lines")).toContainText("created"); + await page.getByRole("button", { name: "Ok, got it!" }).click(); }; diff --git a/kurl_proxy/.gitignore b/kurl_proxy/.gitignore index ae3c172604..5b104eb3a6 100644 --- a/kurl_proxy/.gitignore +++ b/kurl_proxy/.gitignore @@ -1 +1,2 @@ /bin/ +tailwindcss \ No newline at end of file diff --git a/kurl_proxy/assets/images/check.svg b/kurl_proxy/assets/images/check.svg new file mode 100644 index 0000000000..e8bdd9e5dc --- /dev/null +++ b/kurl_proxy/assets/images/check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/kurl_proxy/assets/images/down_arrow.svg b/kurl_proxy/assets/images/down_arrow.svg new file mode 100644 index 0000000000..ca6a25da80 --- /dev/null +++ b/kurl_proxy/assets/images/down_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/kurl_proxy/assets/images/up_arrow.svg b/kurl_proxy/assets/images/up_arrow.svg new file mode 100644 index 0000000000..963d69edde --- /dev/null +++ b/kurl_proxy/assets/images/up_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/kurl_proxy/assets/input.css b/kurl_proxy/assets/input.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/kurl_proxy/assets/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/kurl_proxy/assets/insecure.html b/kurl_proxy/assets/insecure.html index 3f15fc48a4..bc88541707 100644 --- a/kurl_proxy/assets/insecure.html +++ b/kurl_proxy/assets/insecure.html @@ -1,95 +1,261 @@ - - - - + + + + TLS Warning | Admin Console + {{if .AppIcon }} - + {{end}} +
+ -
-
-
-

Bypass browser TLS warning

-

We use a self-signed SSL/TLS Certificate to secure the communication between your local machine and the Admin Console during setup. You'll see a warning about this in your browser, but you can be confident that this is secure.

+
+ {{if .IsEmbeddedCluster }} +
+
+ + Let's get you started! +
-
-
-
-

-

-
-
-
-

Verifying the certificate's authenticity

-
-                    
-                    
-                  
+
+ check + Secure the Admin Console +
+
+ check + + Configure the cluster (optional) + +
+
+ check + Configure {{.AppTitle}} +
+
+ check + + Validate the environment & deploy {{.AppTitle}} + +
+
+ {{end}} +
+

Secure the Admin Console

+
+
+
+

+ We use a self-signed TLS Certificate to secure the communication + between your local machine and the Admin Console during setup. + You'll see a warning about this in your browser, but you can be + confident that this is secure. +

+
+
+
+
+

+ +

+

+ +

-
-
-
-

SHA Fingerprint

-
-                    {{ .fingerprintSHA1 }}
-                  
+ +
+
-
-
- -
+ + +
+
-
- -
+
+ diff --git a/kurl_proxy/assets/output.css b/kurl_proxy/assets/output.css new file mode 100644 index 0000000000..554bb2d275 --- /dev/null +++ b/kurl_proxy/assets/output.css @@ -0,0 +1,792 @@ +*, +::before, +::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +/* +! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ""; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type="button"]), +input:where([type="reset"]), +input:where([type="submit"]) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type="search"] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, +textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +.relative { + position: relative; +} + +.m-auto { + margin: auto; +} + +.mx-6 { + margin-left: 1.5rem; + margin-right: 1.5rem; +} + +.my-8 { + margin-top: 2rem; + margin-bottom: 2rem; +} +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.block { + display: block; +} + +.flex { + display: flex; +} + +.hidden { + display: none; +} + +.h-\[50px\] { + height: 50px; +} + +.min-h-full { + min-height: 100%; +} + +.w-\[400px\] { + width: 400px; +} + +.w-fit { + width: -moz-fit-content; + width: fit-content; +} + +.w-full { + width: 100%; +} + +.max-w-\[1000px\] { + max-width: 1000px; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-auto { + flex: 1 1 auto; +} + +.\!flex-col { + flex-direction: column !important; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-4 { + gap: 1rem; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.border { + border-width: 1px; +} + +.border-solid { + border-style: solid; +} + +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); +} + +.bg-\[\#F9FBFC\] { + --tw-bg-opacity: 1; + background-color: rgb(249 251 252 / var(--tw-bg-opacity)); +} + +.p-4 { + padding: 1rem; +} + +.p-8 { + padding: 2rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pl-8 { + padding-left: 2rem; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-bold { + font-weight: 700; +} + +.font-medium { + font-weight: 500; +} + +.font-normal { + font-weight: 400; +} + +.font-semibold { + font-weight: 600; +} + +.text-\[\#323232\] { + --tw-text-opacity: 1; + color: rgb(50 50 50 / var(--tw-text-opacity)); +} + +.text-\[\#9c9c9c9c\] { + color: #9c9c9c9c; +} + +.shadow-\[0_1px_0_\#c4c8ca\] { + --tw-shadow: 0 1px 0 #c4c8ca; + --tw-shadow-colored: 0 1px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-md { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), + 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} diff --git a/kurl_proxy/assets/tls-custom.css b/kurl_proxy/assets/tls-custom.css index 693ac23899..293abbc451 100644 --- a/kurl_proxy/assets/tls-custom.css +++ b/kurl_proxy/assets/tls-custom.css @@ -3,48 +3,129 @@ License: none (public domain) */ -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display: block; +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; } body { - line-height: 1; + line-height: 1; } -ol, ul { - list-style: none; +ol, +ul { + list-style: none; } -blockquote, q { - quotes: none; +blockquote, +q { + quotes: none; } -blockquote:before, blockquote:after, -q:before, q:after { - content: ''; - content: none; +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; } table { - border-collapse: collapse; - border-spacing: 0; + border-collapse: collapse; + border-spacing: 0; } body, @@ -71,7 +152,7 @@ body a { } .replicated-link { - color: #326DE6; + color: #326de6; cursor: pointer; font-weight: 500; } @@ -107,10 +188,11 @@ code { .hidden-input { opacity: 0; text-indent: -9999px; + position: absolute; } .insecure-image { - width: 502px; + width: 450px; height: 290px; padding-top: 50px; } @@ -145,29 +227,29 @@ code { } .notification.is-warning { - border-color: #BC4752; - color: #BC4752; - background-color: #FBECEA; + border-color: #bc4752; + color: #bc4752; + background-color: #fbecea; width: 250px; } .notification.is-success { border-color: #44bb66; color: #44bb66; - background-color: #EDFFF2; + background-color: #edfff2; } .notification.is-info { border-color: #073551; color: #073551; - background-color: #F5F8FC; + background-color: #f5f8fc; } .form-input { display: block; box-sizing: border-box; width: 100%; - border: 1px solid #DFDFDF; + border: 1px solid #dfdfdf; border-radius: 4px; outline: 0; font-size: 12px; @@ -175,13 +257,13 @@ code { line-height: normal; color: #323232; padding: 7px 12px 8px; - transition: border .2s; + transition: border 0.2s; height: 30px; } .form-input:active, .form-input:focus { - border-color: #326DE6; + border-color: #326de6; } .form-input.larger { @@ -191,18 +273,18 @@ code { .form-input.has-error, .form-input.has-error:focus, .form-input.has-error:active { - border-color: #EE5042; + border-color: #ee5042; } .form-input.is-disabled { user-select: none; cursor: not-allowed; - background-color: #F8F8F8; - border-color: #DFDFDF; + background-color: #f8f8f8; + border-color: #dfdfdf; color: #717171; } .form-input::placeholder { - color: #C4C7CA; + color: #c4c7ca; } .btn { @@ -214,24 +296,24 @@ code { display: inline-block; border: 0; border-radius: 3px; - transition: all .2s; + transition: all 0.2s; outline: none; position: relative; } .btn.primary { - background-color: #326DE6; + background-color: #326de6; border-width: 1px; border-style: solid; border-color: transparent; color: #ffffff; - -webkit-box-shadow: inset 0px -2px 0px 0px rgba(0,0,0,0.25); - -moz-box-shadow: inset 0px -2px 0px 0px rgba(0,0,0,0.25); - box-shadow: inset 0px -2px 0px 0px rgba(0,0,0,0.25); + -webkit-box-shadow: inset 0px -2px 0px 0px rgba(0, 0, 0, 0.25); + -moz-box-shadow: inset 0px -2px 0px 0px rgba(0, 0, 0, 0.25); + box-shadow: inset 0px -2px 0px 0px rgba(0, 0, 0, 0.25); } .btn.primary:not(:disabled):hover { - background-color: #265BC7; + background-color: #265bc7; } .btn.primary.is-disabled, @@ -239,20 +321,17 @@ code { .btn.primary:disabled, .btn.primary[disabled] { cursor: not-allowed; - border-color: #A3D3E9; + border-color: #a3d3e9; color: #ffffff; - background-color: #A3D3E9; + background-color: #a3d3e9; } .btn.secondary { - background-color: #F6F6F6; + background-color: #fff; border-width: 1px; border-style: solid; - border-color: transparent; - color: #326DE6; - -webkit-box-shadow: inset 0px -2px 0px 0px rgba(0,0,0,0.25); - -moz-box-shadow: inset 0px -2px 0px 0px rgba(0,0,0,0.25); - box-shadow: inset 0px -2px 0px 0px rgba(0,0,0,0.25); + border-color: #326de6; + color: #326de6; } .text-muted { @@ -260,7 +339,7 @@ code { font-size: 12px; font-weight: 400; line-height: 20px; - color: #9B9B9B; + color: #9b9b9b; } button.text-button { @@ -269,7 +348,7 @@ button.text-button { padding: 0; cursor: pointer; display: inline-block; - color: #326DE6; + color: #326de6; margin-left: 15px; font-weight: 500; font-size: 12px; @@ -280,20 +359,22 @@ button.text-button:hover { } .flex { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; } .flex-column { - display: flex; + display: flex; flex-direction: column; } -.flex-auto { flex: 0 0 auto; } +.flex-auto { + flex: 0 0 auto; +} .flex1 { - flex: 1; - min-height: 0; + flex: 1; + min-height: 0; } .flex-1-auto { - flex: 1 1 auto; + flex: 1 1 auto; } .u-minHeight--full { @@ -304,12 +385,20 @@ button.text-button:hover { width: 100%; } -.justifyContent--center { justify-content: center; } -.alignContent--center { align-content: center; } +.justifyContent--center { + justify-content: center; +} + +.justifyContent--end { + justify-content: flex-end; +} +.alignContent--center { + align-content: center; +} .borderWrapper { - width:660px; - box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.20); + width: 660px; + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.2); border-radius: 8px; padding: 20px; } @@ -361,7 +450,7 @@ button.text-button:hover { .tls-section-sub { font-size: 14px; font-weight: 400; - color: #9B9B9B; + color: #9b9b9b; line-height: 22px; margin-bottom: 15px; } @@ -369,7 +458,7 @@ button.text-button:hover { .tls-section-sub-sub { font-size: 12px; font-weight: 400; - color: #9B9B9B; + color: #9b9b9b; margin-bottom: 12px; } @@ -379,9 +468,9 @@ button.text-button:hover { .CodeSnippet .CodeSnippet-content { padding: 15px; - border: 1px solid #DFDFDF; + border: 1px solid #dfdfdf; border-radius: 4px; - background-color: #F5F8FC; + background-color: #f5f8fc; } .CodeSnippet .CodeSnippet-content p { @@ -397,7 +486,7 @@ button.text-button:hover { } .inputkey { - top: 40px; + top: 40px; left: 49px; overflow: hidden; position: absolute; @@ -410,11 +499,11 @@ button.text-button:hover { } .inputkey + label { - cursor: pointer; + cursor: pointer; } .inputcert { - top: 40px; + top: 40px; left: 70px; overflow: hidden; position: absolute; @@ -427,7 +516,7 @@ button.text-button:hover { } .inputcert + label { - cursor: pointer; + cursor: pointer; } .appIcon { @@ -442,12 +531,13 @@ button.text-button:hover { box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.3); } .hostname-hint { - color: red; - font-size: 12px; - padding: 2px 0; + color: red; + font-size: 12px; + padding: 2px 0; } -input[type="radio"], label { +input[type="radio"], +label { display: inline-block; font-family: "Helvetica Neue", "Helvetica", sans-serif; font-weight: 400; @@ -466,7 +556,69 @@ div.hidden { p.hidden { display: none; } -#key-label, #cert-label { +#key-label, +#cert-label { max-width: 240px; word-wrap: break-word; -} \ No newline at end of file +} + +.cert-type-box { + width: 150px; + height: 50px; + background: #fff; + border: 1px solid #326de6; + border-radius: 8px; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.checked-background { + color: white; + background: #326de6; +} + +.NavBarWrapper { + width: 100%; + padding: 0 30px; + background-color: #ffffff; + box-shadow: 0 1px 0 #c4c8ca; + margin-bottom: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + height: 50px !important; +} +.HeaderLogo { + margin-right: 20px; +} +.nav-logo { + height: 30px; + width: 30px; + background-position: center; + background-size: contain; + background-repeat: no-repeat; + background-color: #ffffff; + z-index: 1; +} +.tls-content { + width: 800px; +} + +.min-400 { + min-width: 400px; +} +.width-900 { + width: 900px; +} +.cursor { + cursor: pointer; +} + +.copy-command { + font-size: 12px; + padding-top: 8px; +} diff --git a/kurl_proxy/assets/tls.html b/kurl_proxy/assets/tls.html index f2c8578d1e..0ec796588f 100644 --- a/kurl_proxy/assets/tls.html +++ b/kurl_proxy/assets/tls.html @@ -1,99 +1,209 @@ - - - - - - - Configure TLS | Admin Console - - {{if .AppIcon }} + + + + + + Configure TLS | Admin Console + + + {{if .AppIcon }} - {{end}} - - + {{end}} + + - -
-
-
-
-
- {{if .AppIcon }}{{end}} -
-

HTTPS for the {{ .AppTitle }} admin console

+ +
+ -
-

Certificate type

- - - - -
-

- A self-signed TLS certificate is currently used to secure communication - between your browser and the admin console. You will see a warning in your browser - every time you access the admin console unless you upload your own TLS certificate. -

- -
-

- Hostname - (optional) +

+ {{if .IsEmbeddedCluster }} +
+
+ + Let's get you started! + +
+
+ check + Secure the Admin Console +
+
+ check + Configure the cluster (optional) +
+
+ check + Configure {{.AppTitle}} +
+
+ check + + Validate the environment & deploy {{.AppTitle}} + +
+
+ {{end}} +
+

Secure the Admin Console

+
+
+
+

+ Choose whether to continue using a self-signed certificate or + upload your own

-

-

Ensure this domain is routable on your network.

-
- - -
- - + diff --git a/kurl_proxy/assets/tls.js b/kurl_proxy/assets/tls.js index a2b2301986..d58cb4db56 100644 --- a/kurl_proxy/assets/tls.js +++ b/kurl_proxy/assets/tls.js @@ -9,7 +9,7 @@ function startTLS() { if (document.readyState !== "loading") { ready(); } else { - document.addEventListener('DOMContentLoaded', ready); + document.addEventListener("DOMContentLoaded", ready); } function ready() { @@ -17,7 +17,6 @@ function startTLS() { customCertLabels = document.getElementsByClassName("custom-cert-visible"); function handleSubmit(e) { - if (useSelfSigned) { skipAndWait(e); return; @@ -26,6 +25,33 @@ function startTLS() { uploadAndWait(e); } + const selfSignedBtn = document.getElementById("self-signed"); + selfSignedBtn.addEventListener("change", function () { + const certTypeBox = this.closest(".cert-type-box"); + const allCertBoxes = document.querySelectorAll(".cert-type-box"); + if (this.checked) { + allCertBoxes.forEach((box) => { + box.classList.remove("checked-background"); + if (box === certTypeBox) { + box.classList.add("checked-background"); + } + }); + } + }); + const customCertBtn = document.getElementById("custom-cert"); + customCertBtn.addEventListener("change", function () { + const certTypeBox = this.closest(".cert-type-box"); + const allCertBoxes = document.querySelectorAll(".cert-type-box"); + if (this.checked) { + allCertBoxes.forEach((box) => { + box.classList.remove("checked-background"); + if (box === certTypeBox) { + box.classList.add("checked-background"); + } + }); + } + }); + var form = document.getElementById("upload-form"); if (form) { form.addEventListener("submit", handleSubmit); @@ -36,10 +62,10 @@ function startTLS() { skip.addEventListener("click", skipAndWait); } - const typeToggle = document.getElementsByName('type'); + const typeToggle = document.getElementsByName("type"); typeToggle.forEach((el) => { - el.addEventListener('change', handleTypeToggle); + el.addEventListener("change", handleTypeToggle); }); keyInput = document.getElementById("key"); @@ -47,14 +73,14 @@ function startTLS() { keyInput.onchange = (e) => { keyLabel.innerHTML = e.target.files[0].name; - } + }; certInput = document.getElementById("cert"); certLabel = document.getElementById("cert-label"); certInput.onchange = (e) => { certLabel.innerHTML = e.target.files[0].name; - } + }; } function uploadAndWait(e) { @@ -69,11 +95,10 @@ function startTLS() { formData.append("hostname", hostnameInput.value); var xhr = new XMLHttpRequest(); - xhr.onerror = function () { showError(); enableForm(); - } + }; xhr.onloadend = function () { if (xhr.status === 200) { @@ -82,11 +107,11 @@ function startTLS() { } var resp = JSON.parse(xhr.response); - setErrorMsg(resp.error) + setErrorMsg(resp.error); showError(); enableForm(); - } + }; xhr.open("POST", "/tls"); xhr.send(formData); @@ -101,7 +126,7 @@ function startTLS() { var hostnameInput = document.getElementById("hostname"); var formData = new FormData(); - formData.append("hostname", hostnameInput.value) + formData.append("hostname", hostnameInput.value); var xhr = new XMLHttpRequest(); @@ -163,23 +188,27 @@ function startTLS() { } function hideError() { - document.getElementById("error").style.display = 'none'; + document.getElementById("error").style.display = "none"; } function showError() { - document.getElementById("error").style.display = ''; + document.getElementById("error").style.display = ""; } function disableForm() { - document.querySelectorAll("#upload-form input,#upload-form button").forEach(function (el) { - el.disabled = true; - }); + document + .querySelectorAll("#upload-form input,#upload-form button") + .forEach(function (el) { + el.disabled = true; + }); } function enableForm() { - document.querySelectorAll("#upload-form input,#upload-form button").forEach(function (el) { - el.disabled = false; - }); + document + .querySelectorAll("#upload-form input,#upload-form button") + .forEach(function (el) { + el.disabled = false; + }); } function toggleLabels() { @@ -194,10 +223,13 @@ function startTLS() { function handleTypeToggle(e) { if (e && e.target && e.target.value) { - if (e.target.value === "self-signed" || e.target.value === "custom-cert") { + if ( + e.target.value === "self-signed" || + e.target.value === "custom-cert" + ) { toggleLabels(); } } } -}; +} startTLS(); diff --git a/kurl_proxy/assets/welcome.html b/kurl_proxy/assets/welcome.html new file mode 100644 index 0000000000..5ecc7d848e --- /dev/null +++ b/kurl_proxy/assets/welcome.html @@ -0,0 +1,100 @@ + + + + + + + + Welcome to {{.AppTitle}} Admin Console + + + + {{if .AppIcon }} + + {{end}} + + + +
+
+
+ {{if .AppIcon }}{{end}} +
+

{{ .AppTitle }} Admin Console

+
+
+
+
+ {{if .AppIcon }}{{end}} +
+ +

+ Welcome to the {{ .AppTitle }} Admin Console +

+ {{if .IsEmbeddedCluster }} +

+ You will be guided through the setup and installation of + {{ .AppTitle }} +

+

Let's get you started!

+
+
+
+ check +

Secure the Admin Console

+
+
+ check +

+ Configure the cluster (optional) +

+
+
+ check +

Configure {{ .AppTitle }}

+
+
+ check +

+ Validate the environment & deploy {{ .AppTitle }} +

+
+
+ {{end}} +
+ +
+
+
+
+ + diff --git a/kurl_proxy/cmd/main.go b/kurl_proxy/cmd/main.go index 79aa905054..4e7810d75c 100644 --- a/kurl_proxy/cmd/main.go +++ b/kurl_proxy/cmd/main.go @@ -273,8 +273,13 @@ func getHttpServer(fingerprint string, acceptAnonymousUploads bool, assetsDir st if err != nil { log.Printf("No kotsadm application metadata: %v", err) // continue } + appIcon := template.URL(app.Spec.Icon) - c.HTML(http.StatusOK, "insecure.html", gin.H{ + htmlPage := "welcome.html" + if c.Request.URL.Path == "/insecure" { + htmlPage = "insecure.html" + } + c.HTML(http.StatusOK, htmlPage, gin.H{ "fingerprintSHA1": fingerprint, "AppIcon": appIcon, "AppTitle": app.Spec.Title, @@ -317,9 +322,11 @@ func getHttpsServer(upstream, dexUpstream *url.URL, tlsSecretName string, secret } appIcon := template.URL(app.Spec.Icon) c.HTML(http.StatusOK, "tls.html", gin.H{ - "Secret": tlsSecretName, - "AppIcon": appIcon, - "AppTitle": app.Spec.Title, + "Secret": tlsSecretName, + "AppIcon": appIcon, + "AppTitle": app.Spec.Title, + "App": app.Spec, + "IsEmbeddedCluster": isEmbeddedCluster(), }) }) diff --git a/kurl_proxy/tailwind.config.js b/kurl_proxy/tailwind.config.js new file mode 100644 index 0000000000..914424e2a8 --- /dev/null +++ b/kurl_proxy/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./assets/**/*.{html,js}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/web/src/Root.tsx b/web/src/Root.tsx index e2c5048ab1..8f80567f1a 100644 --- a/web/src/Root.tsx +++ b/web/src/Root.tsx @@ -1,4 +1,4 @@ -import { createContext, useEffect, useReducer } from "react"; +import React, { createContext, useEffect, useReducer, useState } from "react"; import { createBrowserHistory } from "history"; import { Navigate, Route, Routes, useNavigate } from "react-router-dom"; import { Helmet } from "react-helmet"; @@ -56,6 +56,9 @@ import AppSnapshotRestore from "@components/snapshots/AppSnapshotRestore"; import EmbeddedClusterViewNode from "@components/apps/EmbeddedClusterViewNode"; import UpgradeStatusModal from "@components/modals/UpgradeStatusModal"; import AppLoading from "@components/apps/AppLoading"; +import Icon from "@components/Icon"; + +import "./scss/components/watches/WatchConfig.scss"; // react-query client const queryClient = new QueryClient(); @@ -75,6 +78,25 @@ const ThemeContext = createContext({ clearThemeState: () => {}, }); +type ConfigGroupItem = { + name: string; + title: string; + type: string; + hidden: boolean; + validationError: boolean; + error: boolean; + when: string; +}; + +type NavbarConfigGroup = { + name: string; + title: string; + items: ConfigGroupItem[]; + hidden: boolean; + hasError: boolean; + when: string; +}; + type AppBranding = { css?: string[]; fontFaces?: string[]; @@ -103,6 +125,7 @@ type State = { snapshotInProgressApps: string[]; isEmbeddedClusterWaitingForNodes: boolean; themeState: ThemeState; + activeGroups: string[]; }; let interval: ReturnType | undefined; @@ -120,6 +143,7 @@ const Root = () => { appSlugFromMetadata: null, appNameSpace: null, adminConsoleMetadata: null, + activeGroups: null, connectionTerminated: false, showUpgradeStatusModal: false, upgradeStatus: "", @@ -450,6 +474,114 @@ const Root = () => { }; const navigate = useNavigate(); + const [currentStep, setCurrentStep] = useState(0); + const [navbarConfigGroups, setNavbarConfigGroups] = useState< + NavbarConfigGroup[] + >([]); + const [activeGroups, setActiveGroups] = useState([]); + + const getStepProps = (step: number) => { + if (step < currentStep) { + return { + icon: "check-circle-filled", + textColor: "tw-text-gray-400", + fontClass: "", + }; + } else if (step === currentStep) { + return { + icon: "check-gray-filled", + textColor: "tw-text-gray-800", + fontClass: "tw-font-bold", + }; + } else { + return { + icon: "check-gray-filled", + textColor: "", + fontClass: "", + }; + } + }; + + const toggleActiveGroups = (name: string) => { + let groupsArr = activeGroups; + if (groupsArr.includes(name)) { + let updatedGroupsArr = groupsArr.filter((n) => n !== name); + setActiveGroups(updatedGroupsArr); + } else { + setActiveGroups([...groupsArr, name]); + } + }; + + const NavGroup = React.memo( + ({ + group, + isActive, + i, + }: { + group: NavbarConfigGroup; + isActive: boolean; + i: number; + }) => { + return ( +
+
toggleActiveGroups(group.name)} + > +
+ {group.title} +
+ {/* adding the arrow-down classes, will rotate the icon when clicked */} + +
+ {group.items ? ( +
+ {group.items + ?.filter((item) => item?.type !== "label") + ?.map((item, j) => { + const hash = location.hash.slice(1); + if (item.hidden || item.when === "false") { + return; + } + return ( + + {item.title} + + ); + })} +
+ ) : null} +
+ ); + } + ); + return ( @@ -493,6 +625,7 @@ const Root = () => { {/* eslint-disable-next-line */} {/* @ts-ignore */} + { isEmbeddedClusterEnabled={Boolean( state.adminConsoleMetadata?.isEmbeddedCluster )} - isEmbeddedClusterWaitingForNodes={ - state.isEmbeddedClusterWaitingForNodes - } isGitOpsSupported={isGitOpsSupported()} isIdentityServiceSupported={isIdentityServiceSupported()} appsList={state.appsList} @@ -511,431 +641,552 @@ const Root = () => { isSnapshotsSupported={isSnapshotsSupported()} errLoggingOut={state.errLoggingOut} /> -
- - - } - /> - } />{" "} - } /> - - } - /> - - } - /> - - } - /> - + {(state.adminConsoleMetadata?.isKurl || + state.adminConsoleMetadata?.isEmbeddedCluster) && + Utilities.isInitialAppInstall(state.appsList[0]) && + Utilities.isLoggedIn() && ( +
+
+ + Let's get you started! + +
+
+ + + Secure the Admin Console + +
+
+ + + Configure the cluster (optional) + +
+
+
+ + + Configure {state.selectedAppName || ""} + +
+ {navbarConfigGroups.length > 0 && ( +
+ {navbarConfigGroups?.map((group, i) => { + if ( + group.title === "" || + group.title.length === 0 || + group.hidden || + group.when === "false" + ) { + return; + } + const isActive = + activeGroups.includes(group.name) || group.hasError; + + return ( + + ); + })} +
)} - /> - } - /> - } /> - } /> - - } - /> - - } - /> - - } - /> - } /> - {state.adminConsoleMetadata?.isEmbeddedCluster && ( - <> - - } - /> - } - /> - +
+
+ + + Validate the environment & deploy{" "} + {state.selectedAppName || ""} + +
+
)} - {(state.adminConsoleMetadata?.isKurl || - state.adminConsoleMetadata?.isEmbeddedCluster) && ( + +
+ - ) : ( - - ) + } /> - )} - {state.adminConsoleMetadata?.isEmbeddedCluster && ( - } - /> - )} - } - /> - - } - /> - {/* :tab? */} - - } - > + } />{" "} + } /> } /> } /> } /> - } /> } /> + } /> } + /> + } /> } - /> - - - } - /> - - } - > - } /> } /> - - } - /> + } /> + {state.adminConsoleMetadata?.isEmbeddedCluster && ( + <> + + } + /> + } + /> + + )} + {(state.adminConsoleMetadata?.isKurl || + state.adminConsoleMetadata?.isEmbeddedCluster) && ( + + ) : ( + + ) + } + /> + )} + {state.adminConsoleMetadata?.isEmbeddedCluster && ( + } + /> + )} } + path="/gitops" + element={} /> } /> + {/* :tab? */} } - /> - - } > } /> } /> } - > - } /> - } /> - } - /> - - } /> - } /> + path="details/:id" + element={ + + } + /> } + path=":slug/:id/restore" + element={} + /> + + } + /> + + } + /> + } /> - } /> } /> } - /> - {/* WHERE IS SELECTEDAPP */} - {state.app?.isAppIdentityServiceSupported && ( + path="/app/*" + element={ + + } + > } + path=":slug" + element={ + + } /> - )} - {/* snapshots redirects */} - } - /> - } - /> - } - /> + + } + /> + + } + /> + } + /> + + } + /> + + } + /> + + } + > + + } + /> + + } + /> + } + > + } /> + } + /> + } + /> + + } /> + } /> + } + /> + } /> + + + } + /> + } + /> + {/* WHERE IS SELECTEDAPP */} + {state.app?.isAppIdentityServiceSupported && ( + } + /> + )} + {/* snapshots redirects */} + } + /> + } + /> + } + /> + + } + /> + + } /> + + } /> - - } /> - - - } - /> - + +
diff --git a/web/src/components/PreflightResultPage.tsx b/web/src/components/PreflightResultPage.tsx index 07662a1967..9b7d8c2777 100644 --- a/web/src/components/PreflightResultPage.tsx +++ b/web/src/components/PreflightResultPage.tsx @@ -22,12 +22,15 @@ import { useDeployAppVersion } from "@features/App/api"; import { KotsParams } from "@types"; import Icon from "./Icon"; -import { useApps } from "@features/App"; +import { useApps, useSelectedApp } from "@features/App"; +import { Utilities } from "@src/utilities/utilities"; interface Props { fromLicenseFlow?: boolean; logo: string; refetchAppsList?: () => void; + setCurrentStep: (step: number) => void; + isEmbeddedCluster: boolean; } function PreflightResultPage(props: Props) { @@ -55,6 +58,7 @@ function PreflightResultPage(props: Props) { // TODO: remove this once everything is using react-query // componentWilUnmount useEffect(() => { + props.setCurrentStep(3); return () => { if (props.fromLicenseFlow && props.refetchAppsList) { props.refetchAppsList(); @@ -71,10 +75,17 @@ function PreflightResultPage(props: Props) { const location = useLocation(); const navigate = useNavigate(); const { refetch: refetchApps } = useApps(); - + const selectedApp = useSelectedApp(); return (
+ {Utilities.isInitialAppInstall(selectedApp) && props.isEmbeddedCluster && ( +
+

+ Validate the environment & deploy {selectedApp?.name} +

+
+ )}
{location.pathname.includes("version-history") && (
navigate(-1)}> @@ -119,9 +130,12 @@ function PreflightResultPage(props: Props) {
)} -

- Preflight checks -

+ {!Utilities.isInitialAppInstall(selectedApp) && + props.isEmbeddedCluster && ( +

+ Preflight checks +

+ )}

Preflight checks validate that your cluster meets the minimum requirements. Required checks must pass in order to deploy the @@ -160,7 +174,7 @@ function PreflightResultPage(props: Props) { > {!location.pathname.includes("version-history") ? "Proceed" - : "Re-run"}{" "} + : "Rerun"}{" "} with limited Preflights

@@ -178,7 +192,7 @@ function PreflightResultPage(props: Props) { className="btn primary blue" onClick={() => rerunPreflights()} > - Re-run + Rerun )}
diff --git a/web/src/components/apps/EmbeddedClusterManagement.tsx b/web/src/components/apps/EmbeddedClusterManagement.tsx index 3fd9c40347..22cacba7a7 100644 --- a/web/src/components/apps/EmbeddedClusterManagement.tsx +++ b/web/src/components/apps/EmbeddedClusterManagement.tsx @@ -31,10 +31,10 @@ type State = { const EmbeddedClusterManagement = ({ fromLicenseFlow = false, - isEmbeddedClusterWaitingForNodes = false, + setCurrentStep, }: { fromLicenseFlow?: boolean; - isEmbeddedClusterWaitingForNodes?: boolean; + setCurrentStep: (step: number) => void; }) => { const [state, setState] = useReducer( (prevState: State, newState: Partial) => ({ @@ -248,6 +248,10 @@ const EmbeddedClusterManagement = ({ } }, [rolesData]); + useEffect(() => { + setCurrentStep(1); + }, []); + const determineDisabledState = () => { return false; }; @@ -417,13 +421,11 @@ const EmbeddedClusterManagement = ({ const AddNodeInstructions = () => { return (
- {Utilities.isInitialAppInstall(app) && ( -

- Optionally add nodes to the cluster. Click{" "} - Continue - to proceed with a single node. -

- )} +

+ Optionally add nodes to the cluster. Click{" "} + Continue + to proceed with a single node. +

{rolesData?.roles && rolesData.roles.length > 1 && @@ -471,7 +473,7 @@ const EmbeddedClusterManagement = ({ >

+
-
+ {Utilities.isInitialAppInstall(app) && ( +
+

+ Configure the cluster +

+
+ )} +

Nodes

-
- {!isInitialInstallOrRestore && ( - <> -
-

- View the nodes in your cluster, generate commands to add nodes - to the cluster, and view workloads running on each node. -

-
- {Utilities.sessionRolesHasOneOf([rbacRoles.CLUSTER_ADMIN]) && ( - - )} - +
+ {" "} + {!Utilities.isInitialAppInstall(app) && ( +
+

+ View the nodes in your cluster, generate commands to add nodes + to the cluster, and view workloads running on each node. +

+
)} + {Utilities.sessionRolesHasOneOf([rbacRoles.CLUSTER_ADMIN]) && + !Utilities.isInitialAppInstall(app) && ( + + )}
- {isInitialInstallOrRestore && ( -
+ {Utilities.isInitialAppInstall(app) && ( +
@@ -618,7 +624,7 @@ const EmbeddedClusterManagement = ({
{fromLicenseFlow && (

{" "} - This is the host at which you can reach the admin console.{" "} + This is the host at which you can reach the Admin Console.{" "}

Select the type of backup you want to perform. A full restore of the - admin console, your application and its metadata, application config + Admin Console, your application and its metadata, application config and your database or a partial restore of your application and its metadata. All data not backed up will be lost and replaced with data in this backup. @@ -103,7 +103,7 @@ export default function BackupRestoreModal(props) { Will this be a full or partial restore?{" "}

- You can do a full restore of the application, admin console, and + You can do a full restore of the application, Admin Console, and databases or you can do a partial restore of just your application and its metadata.

@@ -127,7 +127,7 @@ export default function BackupRestoreModal(props) {

{" "} - Admin console & application{" "} + Admin Console & application{" "}

@@ -169,11 +169,11 @@ export default function BackupRestoreModal(props) {

{" "} - Restore admin console{" "} + Restore Admin Console{" "}

{" "} - Only restores the admin console + Only restores the Admin Console

diff --git a/web/src/components/modals/RestoreSnapshotModal.jsx b/web/src/components/modals/RestoreSnapshotModal.jsx index fe1bbc88cb..02198f748f 100644 --- a/web/src/components/modals/RestoreSnapshotModal.jsx +++ b/web/src/components/modals/RestoreSnapshotModal.jsx @@ -70,7 +70,7 @@ export default function RestoreSnapshotModal(props) { Restoring to this version will remove data and replace it with data from the restored version. During the restoration, your application will not be available and you will not be able to use - the admin console. This action cannot be reversed.{" "} + the Admin Console. This action cannot be reversed.{" "}

diff --git a/web/src/components/selection.json b/web/src/components/selection.json index d9116bdaa7..a46edff536 100644 --- a/web/src/components/selection.json +++ b/web/src/components/selection.json @@ -2101,6 +2101,53 @@ "setId": 0, "iconIdx": 56 }, + { + "icon": { + "paths": [ + "M512 1024c282.769 0 512-229.231 512-512 0-282.77-229.231-512-512-512-282.77 0-512 229.23-512 512 0 282.769 229.23 512 512 512z", + "M240.941 533.588l171.79 171.803c25.536 25.54 63.206 22.661 84.049-6.313l286.278-397.902" + ], + "attrs": [ + { "fill": "rgb(155, 155, 155)" }, + { + "fill": "none", + "stroke": "rgb(255, 255, 255)", + "strokeLinejoin": "miter", + "strokeLinecap": "round", + "strokeMiterlimit": "4", + "strokeWidth": 60.23529411764706 + } + ], + "isMulticolor": false, + "isMulticolor2": true, + "grid": 0, + "tags": ["check-gray-filled"], + "colorPermutations": { + "15515515512552552551": [{ "f": 0 }, { "s": 1 }] + } + }, + "attrs": [ + { "fill": "rgb(155, 155, 155)" }, + { + "fill": "none", + "stroke": "rgb(255, 255, 255)", + "strokeLinejoin": "miter", + "strokeLinecap": "round", + "strokeMiterlimit": "4", + "strokeWidth": 60.23529411764706 + } + ], + "properties": { + "order": 11, + "id": 0, + "name": "check-gray-filled", + "prevSize": 32, + "code": 59648 + }, + "setIdx": 0, + "setId": 6, + "iconIdx": 0 + }, { "icon": { "paths": [ diff --git a/web/src/components/snapshots/AppSnapshotRestore.jsx b/web/src/components/snapshots/AppSnapshotRestore.jsx index e034f53ff5..6aeee9fa34 100644 --- a/web/src/components/snapshots/AppSnapshotRestore.jsx +++ b/web/src/components/snapshots/AppSnapshotRestore.jsx @@ -337,7 +337,7 @@ class AppSnapshotRestore extends Component {

{" "} After all volumes have been restored you will need to log back in - to the admin console.{" "} + to the Admin Console.{" "}

{restoreLoading && ( diff --git a/web/src/components/snapshots/SnapshotRestore.jsx b/web/src/components/snapshots/SnapshotRestore.jsx index 56faeb1057..ac2efcd288 100644 --- a/web/src/components/snapshots/SnapshotRestore.jsx +++ b/web/src/components/snapshots/SnapshotRestore.jsx @@ -354,7 +354,7 @@ class SnapshotRestore extends Component {

{" "} After all volumes have been restored you will need to log back in - to the admin console.{" "} + to the Admin Console.{" "}

{restoreLoading && ( diff --git a/web/src/components/snapshots/SnapshotSchedule.jsx b/web/src/components/snapshots/SnapshotSchedule.jsx index eab466f78f..a997086196 100644 --- a/web/src/components/snapshots/SnapshotSchedule.jsx +++ b/web/src/components/snapshots/SnapshotSchedule.jsx @@ -539,7 +539,7 @@ class SnapshotSchedule extends Component {

Scheduled {featureName}s

- Configure a schedule for {featureName}s of the admin console and + Configure a schedule for {featureName}s of the Admin Console and all application data.

diff --git a/web/src/components/snapshots/Snapshots.jsx b/web/src/components/snapshots/Snapshots.jsx index 8ad94f72a7..a2607c68f3 100644 --- a/web/src/components/snapshots/Snapshots.jsx +++ b/web/src/components/snapshots/Snapshots.jsx @@ -597,7 +597,7 @@ class Snapshots extends Component {

- Back up the admin console and all application data for + Back up the Admin Console and all application data for disaster recovery.{" "} {!this.props.isEmbeddedCluster && ( <> diff --git a/web/src/components/troubleshoot/GenerateSupportBundleModal.tsx b/web/src/components/troubleshoot/GenerateSupportBundleModal.tsx index 45d9d471e6..266e482d5d 100644 --- a/web/src/components/troubleshoot/GenerateSupportBundleModal.tsx +++ b/web/src/components/troubleshoot/GenerateSupportBundleModal.tsx @@ -318,7 +318,7 @@ const GenerateSupportBundleModal = ({

Run the following commands to generate a support bundle from the CLI. You can then upload a support bundle so that it - appears in the admin console. + appears in the Admin Console.

{" "} to get a command to manually generate a support bundle. This is - useful if the admin console is inaccessible. + useful if the Admin Console is inaccessible.
) diff --git a/web/src/components/upgrade_service/PreflightChecks.tsx b/web/src/components/upgrade_service/PreflightChecks.tsx index 8852bb8555..63c336dbdb 100644 --- a/web/src/components/upgrade_service/PreflightChecks.tsx +++ b/web/src/components/upgrade_service/PreflightChecks.tsx @@ -115,7 +115,7 @@ const PreflightCheck = ({

{preflightCheck?.showPreflightCheckPending && ( -
+
runPreflights()} > - Re-run + Rerun )}
diff --git a/web/src/features/AddNewApp/components/InstallWithHelm.tsx b/web/src/features/AddNewApp/components/InstallWithHelm.tsx index d6eb7687fa..3f959d176e 100644 --- a/web/src/features/AddNewApp/components/InstallWithHelm.tsx +++ b/web/src/features/AddNewApp/components/InstallWithHelm.tsx @@ -30,7 +30,7 @@ function InstallWithHelm() { className="u-fontSize--normal u-textColor--accent u-fontWeight--medium u-lineHeight--normal u-marginTop--20 u-marginRight--30 u-marginLeft--30" style={{ maxWidth: "300px" }} > - In order to use the admin console you need to install a Helm + In order to use the Admin Console you need to install a Helm chart.

diff --git a/web/src/features/AdminConsole/api/getAdminConsoleUpdateStatus.tsx b/web/src/features/AdminConsole/api/getAdminConsoleUpdateStatus.tsx index bfb6b3defd..f58825a399 100644 --- a/web/src/features/AdminConsole/api/getAdminConsoleUpdateStatus.tsx +++ b/web/src/features/AdminConsole/api/getAdminConsoleUpdateStatus.tsx @@ -21,7 +21,7 @@ async function getAdminConsoleUpdateStatus({ if (!response.ok) { throw new Error( - `Error while trying to get admin console update status: ${response.status}` + `Error while trying to get Admin Console update status: ${response.status}` ); } @@ -48,7 +48,7 @@ function useAdminConsoleUpdateStatus({ slug }: { slug: string }) { console.log(err); throw new Error( err.message || - "Error while trying to get admin console update status. Please try again." + "Error while trying to get Admin Console update status. Please try again." ); }, }); diff --git a/web/src/features/AdminConsole/api/postUpdateAdminConsole.tsx b/web/src/features/AdminConsole/api/postUpdateAdminConsole.tsx index af52232b6c..33748480f6 100644 --- a/web/src/features/AdminConsole/api/postUpdateAdminConsole.tsx +++ b/web/src/features/AdminConsole/api/postUpdateAdminConsole.tsx @@ -23,7 +23,7 @@ async function postUpdateAdminConsole({ if (!response.ok) { throw new Error( - `Error while trying to update admin console: ${response.status}` + `Error while trying to update Admin Console: ${response.status}` ); } @@ -32,11 +32,11 @@ async function postUpdateAdminConsole({ } catch (err) { if (err instanceof Error) { throw new Error( - `Error while trying to unmarshal update admin console response: ${err.message}` + `Error while trying to unmarshal update Admin Console response: ${err.message}` ); } throw new Error( - `Error while trying to unmarshal update admin console response` + `Error while trying to unmarshal update Admin Console response` ); } } @@ -59,7 +59,7 @@ function useUpdateAdminConsole({ console.log(err); throw new Error( err.message || - "Error while trying to update admin console. Please try again." + "Error while trying to update Admin Console. Please try again." ); }, onSuccess: () => { diff --git a/web/src/features/AppConfig/components/AppConfig.tsx b/web/src/features/AppConfig/components/AppConfig.tsx index a26e0ed557..76a587f0db 100644 --- a/web/src/features/AppConfig/components/AppConfig.tsx +++ b/web/src/features/AppConfig/components/AppConfig.tsx @@ -29,6 +29,9 @@ type Props = { refreshAppData: () => void; refetchApps: () => void; navigate: ReturnType; + setCurrentStep: (step: number) => void; + setNavbarConfigGroups: (ConfigGroup) => void; + setActiveGroups: (ConfigGroup) => void; }; // This was typed from the implementation of the component so it might be wrong @@ -126,6 +129,7 @@ class AppConfig extends Component { } componentDidMount() { + this.props.setCurrentStep(2); const { app, navigate } = this.props; if (app && !app.isConfigurable) { // app not configurable - redirect @@ -251,9 +255,15 @@ class AppConfig extends Component { changed: false, configLoading: false, }); + if (this.props.isEmbeddedCluster) { + this.props.setNavbarConfigGroups(data.configGroups); + } if (this.props.location.hash.length > 0) { this.navigateToCurrentHash(); } else { + if (this.props.isEmbeddedCluster) { + this.props.setActiveGroups([data.configGroups[0].name]); + } this.setState({ activeGroups: [data.configGroups[0].name], configLoading: false, @@ -385,6 +395,9 @@ class AppConfig extends Component { configGroups: newGroups, showValidationError: hasValidationError, }); + if (this.props.isEmbeddedCluster) { + this.props.setNavbarConfigGroups(newGroups); + } if (result.error) { this.setState({ showConfigError: Boolean(result.error), @@ -589,6 +602,9 @@ class AppConfig extends Component { }); const changed = this.isConfigChanged(newGroups); this.setState({ configGroups: newGroups, changed }); + if (this.props.isEmbeddedCluster) { + this.props.setNavbarConfigGroups({ newGroups, changed }); + } }) .catch((error) => { if (error.name !== "AbortError") { @@ -758,69 +774,70 @@ class AppConfig extends Component { }); return ( -
+
-
- {fromLicenseFlow && app && ( - - Configure {app.name} - - )} +
+

+ Configure {app.name} +

+
+
-
- {configGroups?.map((group, i) => { - if ( - group.title === "" || - group.title.length === 0 || - group.hidden || - group.when === "false" - ) { - return; - } - return ( -
+ {!this.props.isEmbeddedCluster && ( +
+ {configGroups?.map((group, i) => { + if ( + group.title === "" || + group.title.length === 0 || + group.hidden || + group.when === "false" + ) { + return; + } + return (
this.toggleActiveGroups(group.name)} + key={`${i}-${group.name}-${group.title}`} + className={`side-nav-group ${ + this.state.activeGroups.includes(group.name) || + group.hasError + ? "group-open" + : "" + }`} + id={`config-group-nav-${group.name}`} > -
- {group.title} +
this.toggleActiveGroups(group.name)} + > +
+ {group.title} +
+ {/* adding the arrow-down classes, will rotate the icon when clicked */} +
- {/* adding the arrow-down classes, will rotate the icon when clicked */} - -
- {group.items ? ( - - ) : null} -
- ); - })} -
-
+ href={`#${item.name}-group`} + key={`${j}-${item.name}-${item.title}`} + > + {item.title} + + ); + })} +
+ ) : null} +
+ ); + })} +
+ )} +
@@ -865,21 +882,21 @@ class AppConfig extends Component { readonly={this.isConfigReadOnly(app)} />
-
+
{savingConfig && (
)} {!savingConfig && ( -
+
{(showConfigError || this.state.showValidationError) && ( {configErrorMessage || validationErrorMessage} )}
{" "}
- { > Log in {appName && appName !== "" - ? ` to ${appName} admin console` - : " to admin console"} + ? ` to ${appName} Admin Console` + : " to Admin Console"}

- Enter the password to access the {appName} admin console. + Enter the password to access the {appName} Admin Console.

{loginErr && ( @@ -386,7 +386,7 @@ class SecureAdminConsole extends Component { }} />
-
+
diff --git a/web/src/features/Gitops/components/AppGitops.jsx b/web/src/features/Gitops/components/AppGitops.jsx index f031111147..f3eaa0dd23 100644 --- a/web/src/features/Gitops/components/AppGitops.jsx +++ b/web/src/features/Gitops/components/AppGitops.jsx @@ -187,7 +187,7 @@ const AppGitops = () => {

Connect a git version control system so all application updates are committed to a git repository.
When GitOps is enabled, you - cannot deploy updates directly from the admin console. + cannot deploy updates directly from the Admin Console.

diff --git a/web/src/features/Gitops/components/SetupProvider.jsx b/web/src/features/Gitops/components/SetupProvider.jsx index 63b57330df..4caee92fae 100644 --- a/web/src/features/Gitops/components/SetupProvider.jsx +++ b/web/src/features/Gitops/components/SetupProvider.jsx @@ -97,7 +97,7 @@ const SetupProvider = ({ appName }) => { Connect a git version control system so all application updates are committed to a git repository.
When GitOps is enabled, you cannot deploy updates directly from - the admin console. + the Admin Console.

diff --git a/web/src/features/Gitops/components/modals/DisableModal.jsx b/web/src/features/Gitops/components/modals/DisableModal.jsx index 6ac38065ee..e274728ede 100644 --- a/web/src/features/Gitops/components/modals/DisableModal.jsx +++ b/web/src/features/Gitops/components/modals/DisableModal.jsx @@ -29,7 +29,7 @@ const DisableModal = ({ isOpen, setOpen, disableGitOps, provider }) => {

Commits will no longer be made to your repository, and you will have to
- deploy from the admin console. + deploy from the Admin Console.