From 28aa8d2b16eafb5ab52f52ea2232fc3ed47768a3 Mon Sep 17 00:00:00 2001 From: bruno-sachin Date: Sat, 27 Jun 2026 23:22:04 +0530 Subject: [PATCH] feat: Environment UI revamp changes --- .../environment-table.component.ts | 33 +++ .../oc-docs/e2e/pages/environments.page.ts | 27 +++ .../oc-docs/e2e/playwright/pages.fixture.ts | 29 +++ .../tests/environments/environments.spec.ts | 88 ++++++++ packages/oc-docs/src/components/Docs/Docs.tsx | 32 ++- .../src/components/Docs/Sidebar/Sidebar.tsx | 37 +++- .../components/Docs/Sidebar/StyledWrapper.ts | 18 ++ .../EnvironmentLabel.spec.tsx | 25 +++ .../EnvironmentLabel/EnvironmentLabel.tsx | 25 +++ .../EnvironmentLabel/StyledWrapper.ts | 23 ++ .../EnvironmentSummaryItem.tsx | 8 +- .../EnvironmentSummaryItem/StyledWrapper.ts | 13 +- .../pages/Environments/Environments.spec.tsx | 95 +++++++++ .../src/pages/Environments/Environments.tsx | 197 ++++++++++++++++++ .../src/pages/Environments/StyledWrapper.ts | 103 +++++++++ packages/oc-docs/src/sampleCollection.ts | 17 +- packages/oc-docs/src/store/slices/docs.ts | 15 +- .../oc-docs/src/ui/Table/StyledWrapper.ts | 133 ++++++++++++ packages/oc-docs/src/ui/Table/Table.spec.tsx | 53 +++++ packages/oc-docs/src/ui/Table/Table.tsx | 140 +++++++++++++ .../oc-docs/src/utils/environments.spec.ts | 130 ++++++++++++ packages/oc-docs/src/utils/environments.ts | 128 ++++++++++++ 22 files changed, 1338 insertions(+), 31 deletions(-) create mode 100644 packages/oc-docs/e2e/components/environments/environment-table.component.ts create mode 100644 packages/oc-docs/e2e/pages/environments.page.ts create mode 100644 packages/oc-docs/e2e/tests/environments/environments.spec.ts create mode 100644 packages/oc-docs/src/components/EnvironmentLabel/EnvironmentLabel.spec.tsx create mode 100644 packages/oc-docs/src/components/EnvironmentLabel/EnvironmentLabel.tsx create mode 100644 packages/oc-docs/src/components/EnvironmentLabel/StyledWrapper.ts create mode 100644 packages/oc-docs/src/pages/Environments/Environments.spec.tsx create mode 100644 packages/oc-docs/src/pages/Environments/Environments.tsx create mode 100644 packages/oc-docs/src/pages/Environments/StyledWrapper.ts create mode 100644 packages/oc-docs/src/ui/Table/StyledWrapper.ts create mode 100644 packages/oc-docs/src/ui/Table/Table.spec.tsx create mode 100644 packages/oc-docs/src/ui/Table/Table.tsx create mode 100644 packages/oc-docs/src/utils/environments.spec.ts create mode 100644 packages/oc-docs/src/utils/environments.ts diff --git a/packages/oc-docs/e2e/components/environments/environment-table.component.ts b/packages/oc-docs/e2e/components/environments/environment-table.component.ts new file mode 100644 index 0000000..5d55d2d --- /dev/null +++ b/packages/oc-docs/e2e/components/environments/environment-table.component.ts @@ -0,0 +1,33 @@ +import type { Locator } from '@playwright/test'; +import { BaseComponent } from '../base.component'; + +export class EnvironmentTableComponent extends BaseComponent { + readonly root = this.page.getByTestId('environment-table'); + readonly variableRows = this.page.getByTestId('environment-variable-row'); + readonly secretVariableRows = this.page.getByTestId('environment-secret-variable-row'); + readonly externalSecretRows = this.page.getByTestId('environment-external-secret-row'); + + columnHeader(key: string): Locator { + return this.root.getByTestId(`table-header-${key}`); + } + + variableRow(name: string): Locator { + return this.variableRows.filter({ hasText: name }); + } + + secretVariableRow(name: string): Locator { + return this.secretVariableRows.filter({ hasText: name }); + } + + externalSecretRow(name: string): Locator { + return this.externalSecretRows.filter({ hasText: name }); + } + + valueOf(name: string): Locator { + return this.variableRow(name).getByTestId('table-cell-value'); + } + + dataTypeOf(name: string): Locator { + return this.variableRow(name).getByTestId('table-cell-type'); + } +} diff --git a/packages/oc-docs/e2e/pages/environments.page.ts b/packages/oc-docs/e2e/pages/environments.page.ts new file mode 100644 index 0000000..859d350 --- /dev/null +++ b/packages/oc-docs/e2e/pages/environments.page.ts @@ -0,0 +1,27 @@ +import type { Locator } from '@playwright/test'; +import { BasePage } from './base.page'; +import { SidebarComponent } from '../components/sidebar.component'; +import { EnvironmentTableComponent } from '../components/environments/environment-table.component'; + +export class EnvironmentsPage extends BasePage { + readonly root = this.page.getByTestId('environments-page'); + + readonly sidebar = new SidebarComponent(this.page); + readonly title = this.page.getByTestId('environments-title'); + readonly tabs = this.page.getByTestId('environment-tab'); + readonly table = new EnvironmentTableComponent(this.page); + readonly variablesGroup = this.page.getByTestId('environment-variables'); + readonly secretVariablesGroup = this.page.getByTestId('environment-secret-variables'); + readonly externalSecretsGroup = this.page.getByTestId('environment-external-secrets'); + readonly emptyState = this.page.getByTestId('environments-empty'); + + async open(): Promise { + await this.navigate('/'); + await this.page.getByTestId('sidebar-environments-link').click(); + await this.root.waitFor({ state: 'visible' }); + } + + tab(name: string): Locator { + return this.tabs.filter({ hasText: name }); + } +} diff --git a/packages/oc-docs/e2e/playwright/pages.fixture.ts b/packages/oc-docs/e2e/playwright/pages.fixture.ts index 065ea12..493a993 100644 --- a/packages/oc-docs/e2e/playwright/pages.fixture.ts +++ b/packages/oc-docs/e2e/playwright/pages.fixture.ts @@ -1,5 +1,10 @@ import { test as base } from '@playwright/test'; import { OverviewPage } from '../pages/overview.page'; +import { EnvironmentsPage } from '../pages/environments.page'; +import { RequestPage } from '../pages/request.page'; +import { ScriptPage } from '../pages/script.page'; +import { UnsupportedRequestPage } from '../pages/unsupported-request.page'; +import { SidebarComponent } from '../components/sidebar.component'; import { ThemeToggleComponent } from '../components/theme-toggle.component'; /** @@ -8,6 +13,12 @@ import { ThemeToggleComponent } from '../components/theme-toggle.component'; */ type Fixtures = { overviewPage: OverviewPage; + environmentsPage: EnvironmentsPage; + requestPage: RequestPage; + scriptPage: ScriptPage; + unsupportedRequestPage: UnsupportedRequestPage; + sidebar: SidebarComponent; + pageHeader: PageHeaderComponent; themeToggle: ThemeToggleComponent; }; @@ -15,6 +26,24 @@ export const test = base.extend({ overviewPage: async ({ page }, use) => { await use(new OverviewPage(page)); }, + environmentsPage: async ({ page }, use) => { + await use(new EnvironmentsPage(page)); + }, + requestPage: async ({ page }, use) => { + await use(new RequestPage(page)); + }, + scriptPage: async ({ page }, use) => { + await use(new ScriptPage(page)); + }, + unsupportedRequestPage: async ({ page }, use) => { + await use(new UnsupportedRequestPage(page)); + }, + sidebar: async ({ page }, use) => { + await use(new SidebarComponent(page)); + }, + pageHeader: async ({ page }, use) => { + await use(new PageHeaderComponent(page)); + }, themeToggle: async ({ page }, use) => { await use(new ThemeToggleComponent(page)); } diff --git a/packages/oc-docs/e2e/tests/environments/environments.spec.ts b/packages/oc-docs/e2e/tests/environments/environments.spec.ts new file mode 100644 index 0000000..f8991f1 --- /dev/null +++ b/packages/oc-docs/e2e/tests/environments/environments.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '../../playwright'; + +test.describe('Environments page', () => { + test.beforeEach(async ({ environmentsPage }) => { + await environmentsPage.open(); + }); + + test('opens from the sidebar and shows a tab per environment', async ({ environmentsPage }) => { + await expect(environmentsPage.title).toHaveText('Environments'); + await expect(environmentsPage.tabs).toHaveCount(2); + await expect(environmentsPage.tab('Local')).toBeVisible(); + await expect(environmentsPage.tab('Prod')).toBeVisible(); + }); + + test('selects the first environment by default', async ({ environmentsPage }) => { + await expect(environmentsPage.tab('Local')).toHaveAttribute('aria-selected', 'true'); + await expect(environmentsPage.tab('Prod')).toHaveAttribute('aria-selected', 'false'); + }); + + test('lists the active environment variables with their value and data type', async ({ environmentsPage }) => { + const { table } = environmentsPage; + + await test.step('the Variables group is shown', async () => { + await expect(environmentsPage.variablesGroup).toBeVisible(); + }); + + await test.step('the host variable shows its value and a String data type', async () => { + await expect(table.variableRow('host')).toBeVisible(); + await expect(table.valueOf('host')).toContainText('http://localhost:8081'); + await expect(table.dataTypeOf('host')).toHaveText('String'); + }); + }); + + test('shows secret variables masked under a Secret Variables group', async ({ environmentsPage }) => { + const { table } = environmentsPage; + const row = table.secretVariableRow('bearer_auth_token'); + + await expect(environmentsPage.secretVariablesGroup).toBeVisible(); + await expect(row).toBeVisible(); + await expect(row).toContainText('Secret'); + await expect(row).not.toContainText('your_secret_token'); + }); + + test('switches the table when another environment tab is selected', async ({ environmentsPage }) => { + const { table } = environmentsPage; + + await expect(table.valueOf('host')).toContainText('http://localhost:8081'); + + await environmentsPage.tab('Prod').click(); + + await expect(environmentsPage.tab('Prod')).toHaveAttribute('aria-selected', 'true'); + await expect(table.valueOf('host')).toContainText('https://echo.usebruno.com'); + }); + + test('renders an accessible columnar table with Name, Value and Data Type headers', async ({ environmentsPage }) => { + const { table } = environmentsPage; + await expect(table.columnHeader('name')).toHaveText('Name'); + await expect(table.columnHeader('value')).toHaveText('Value'); + await expect(table.columnHeader('type')).toHaveText('Data Type'); + }); + + test('hides the External Secret Variables section when the environment has none', async ({ environmentsPage }) => { + await expect(environmentsPage.tab('Local')).toHaveAttribute('aria-selected', 'true'); + await expect(environmentsPage.externalSecretsGroup).toBeHidden(); + }); + + test('shows external secrets with the manager label and reference for the Prod environment', async ({ + environmentsPage + }) => { + const { table } = environmentsPage; + + await environmentsPage.tab('Prod').click(); + + await test.step('the External Secret Variables group is shown with its manager', async () => { + await expect(environmentsPage.externalSecretsGroup).toBeVisible(); + await expect(environmentsPage.externalSecretsGroup).toContainText('AWS Secrets Manager'); + }); + + await test.step('each external secret lists its name and secret reference', async () => { + await expect(table.externalSecretRow('dbPassword')).toContainText('prod/db/credentials'); + await expect(table.externalSecretRow('apiKey')).toContainText('prod/payment-gateway/api-key'); + }); + + await test.step('external secrets without a type show an empty data type', async () => { + await expect(table.externalSecretRow('dbPassword')).toContainText('(empty)'); + }); + }); +}); diff --git a/packages/oc-docs/src/components/Docs/Docs.tsx b/packages/oc-docs/src/components/Docs/Docs.tsx index 7981d44..38824d2 100644 --- a/packages/oc-docs/src/components/Docs/Docs.tsx +++ b/packages/oc-docs/src/components/Docs/Docs.tsx @@ -4,8 +4,13 @@ import Sidebar from './Sidebar/Sidebar'; import Overview from '../../pages/Overview/Overview'; import { getItemId, generateSafeId } from '../../utils/itemUtils'; import { isFolder } from '../../utils/schemaHelpers'; -import { useAppSelector } from '../../store/hooks'; -import { selectSelectedItemId } from '../../store/slices/docs'; +import Environments from '../../pages/Environments/Environments'; +import Request from '../../pages/Request/Request'; +import Script from '../../pages/Script/Script'; +import { findItemByUuid, getAncestorsByUuid } from '../../utils/fileUtils'; +import { isHttpRequest, isScriptFile, isUnsupportedRequest } from '../../utils/schemaHelpers'; +import { useAppSelector, useAppDispatch } from '../../store/hooks'; +import { selectSelectedItemId, selectActiveRootView, selectItem } from '../../store/slices/docs'; interface DocsProps { docsCollection: OpenCollectionCollection | null; @@ -19,6 +24,7 @@ const Docs: React.FC = ({ }) => { const selectedItemId = useAppSelector(selectSelectedItemId); const isInitialMount = useRef(true); + const activeRootView = useAppSelector(selectActiveRootView); // Scroll to selected item when it changes (but not on initial load) useEffect(() => { @@ -76,10 +82,24 @@ const Docs: React.FC = ({ -
- {docsCollection && ( +
+ {docsCollection && (isHttpRequest(selected) || isUnsupportedRequest(selected)) ? ( + onOpenPlayground?.()} + onBreadcrumbClick={(uuid) => dispatch(selectItem(uuid))} + /> + ) : docsCollection && isScriptFile(selected) ? ( +