Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6afe80b
OpenAPI types
ivosh Feb 27, 2026
103127e
ProxiesList
ivosh Feb 27, 2026
f7703e5
Filter by status
ivosh Feb 27, 2026
b328e6b
Refactor
ivosh Feb 27, 2026
0c72c99
Create Proxy dialog
ivosh Feb 27, 2026
aadf11a
ProxyDetail
ivosh Feb 27, 2026
cb9ec53
Bulk-delete epic for Proxies
ivosh Feb 28, 2026
f659ac0
Address review issues by Copilot
ivosh Mar 1, 2026
0bc65d0
Tests for ducks/proxies and ducks/proxies-epic
ivosh Mar 1, 2026
4b76cba
Revert "Tests for ducks/proxies and ducks/proxies-epic"
ivosh Mar 1, 2026
24ceb4e
Installation instructions
ivosh Mar 2, 2026
a575bde
Wire-up installation instructions
ivosh Mar 2, 2026
deb3fac
Reapply "Tests for ducks/proxies and ducks/proxies-epic"
ivosh Mar 2, 2026
309e94b
Button to show installation instructions
ivosh Mar 2, 2026
0b14a5d
Show managed connectors
ivosh Mar 3, 2026
3e34598
Address review issues by Copilot
ivosh Mar 3, 2026
b5b2034
Test with vitest
ivosh Mar 5, 2026
d9a6813
Add missing coverage
ivosh Mar 5, 2026
71cebdd
Fix security hotspots reported by SonarQube
ivosh Mar 5, 2026
c589ed7
Merge branch 'main' of github.com:CZERTAINLY/CZERTAINLY-FE-Administra…
soloviovmax Mar 11, 2026
4899f58
Merge branch 'main' of github.com:CZERTAINLY/CZERTAINLY-FE-Administra…
soloviovmax Mar 12, 2026
cd53d18
Merge branch 'main' into feat/proxies
lubomirw Mar 12, 2026
4cecb75
Merge branch 'main' into feat/proxies
ivosh Mar 13, 2026
e805d18
Fix some SonarQube issues
ivosh Mar 13, 2026
2afe456
Link to a proxy in connector list and detail
ivosh Mar 13, 2026
497038e
Format properly lastActivity
ivosh Mar 13, 2026
17c4445
Fix issues raised by Copilot
ivosh Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { backendClient, updateBackendUtilsClients } from './api';

describe('api', () => {
beforeEach(() => {
// Reset utils clients before each test
updateBackendUtilsClients(undefined);
});

it('should have backendClient initialized', () => {
expect(backendClient).toBeDefined();
expect(backendClient.auth).toBeDefined();
expect(backendClient.users).toBeDefined();
expect(backendClient.roles).toBeDefined();
expect(backendClient.certificates).toBeDefined();
expect(backendClient.raProfiles).toBeDefined();
});

it('should update backend utils clients when a valid URL is provided', () => {
const testUrl = 'https://test-utils-api.com';
updateBackendUtilsClients(testUrl);

expect(backendClient.utilsCertificate).toBeDefined();
expect(backendClient.utilsOid).toBeDefined();
expect(backendClient.utilsCertificateRequest).toBeDefined();
expect(backendClient.utilsActuator).toBeDefined();
});

it('should clear backend utils clients when an empty URL is provided', () => {
// First set them
updateBackendUtilsClients('https://test-utils-api.com');
expect(backendClient.utilsCertificate).toBeDefined();

// Then clear them
updateBackendUtilsClients('');
expect(backendClient.utilsCertificate).toBeUndefined();
expect(backendClient.utilsOid).toBeUndefined();
expect(backendClient.utilsCertificateRequest).toBeUndefined();
expect(backendClient.utilsActuator).toBeUndefined();
});

it('should clear backend utils clients when undefined is provided', () => {
// First set them
updateBackendUtilsClients('https://test-utils-api.com');
expect(backendClient.utilsCertificate).toBeDefined();

// Then clear them
updateBackendUtilsClients(undefined);
expect(backendClient.utilsCertificate).toBeUndefined();
expect(backendClient.utilsOid).toBeUndefined();
expect(backendClient.utilsCertificateRequest).toBeUndefined();
expect(backendClient.utilsActuator).toBeUndefined();
});
});
7 changes: 5 additions & 2 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
InternalNotificationApi,
LocationManagementApi,
NotificationProfileInventoryApi,
ProxyManagementApi,
OAuth2LoginManagementV2Api,
RAProfileManagementApi,
ResourceManagementApi,
Expand All @@ -54,10 +55,10 @@
CertificateUtilsAPIApi,
CertificationRequestUtilsAPIApi,
Configuration as ConfigurationUtils,
OIDUtilsAPIApi,
} from 'types/openapi/utils';
import { OIDUtilsAPIApi } from './types/openapi/utils';

const apiUrl = (window as any).__ENV__?.API_URL || '/api';
const apiUrl = typeof window !== 'undefined' ? window?.__ENV__?.API_URL || '/api' : '/api';

Check warning on line 61 in src/api.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytt9yp0fdyHr9G3PU-&open=AZytt9yp0fdyHr9G3PU-&pullRequest=1272

Check warning on line 61 in src/api.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytt9yp0fdyHr9G3PU_&open=AZytt9yp0fdyHr9G3PU_&pullRequest=1272

Check warning on line 61 in src/api.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytt9yp0fdyHr9G3PVA&open=AZytt9yp0fdyHr9G3PVA&pullRequest=1272
const configuration = new Configuration({ basePath: apiUrl });

export interface ApiClients {
Expand All @@ -72,6 +73,7 @@
credentials: CredentialManagementApi;
connectors: ConnectorManagementApi;
connectorsV2: ConnectorManagementV2Api;
proxies: ProxyManagementApi;
callback: CallbackApi;
statisticsDashboard: StatisticsDashboardApi;
authorities: AuthorityManagementApi;
Expand Down Expand Up @@ -134,6 +136,7 @@
login: new OAuth2LoginManagementV2Api(configuration),
notificationProfiles: new NotificationProfileInventoryApi(configuration),
connectors: new ConnectorManagementApi(configuration),
proxies: new ProxyManagementApi(configuration),
callback: new CallbackApi(configuration),
statisticsDashboard: new StatisticsDashboardApi(configuration),
acmeAccounts: new ACMEAccountManagementApi(configuration),
Expand Down
10 changes: 10 additions & 0 deletions src/components/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import ComplianceProfilesList from './_pages/compliance-profiles/list';
import ConnectorDetail from './_pages/connectors/detail';
import ConnectorsList from './_pages/connectors/list';

import { ProxyDetail } from './_pages/proxies/detail/ProxyDetail';
import { ProxiesList } from './_pages/proxies/list/ProxiesList';

import ApprovalProfileDetails from './_pages/approval-profiles/detail';
import ApprovalProfiles from './_pages/approval-profiles/list';

Expand Down Expand Up @@ -165,6 +168,13 @@ export default function AppRouter() {
/>
<Route path={`/${Resource.Connectors.toLowerCase()}/detail/:id`} element={<ConnectorDetail />} />

<Route path={`/${Resource.Proxies.toLowerCase()}`} element={<ProxiesList />} />
<Route
path={`/${Resource.Proxies.toLowerCase()}/list`}
element={<Navigate to={`/${Resource.Proxies.toLowerCase()}`} />}
/>
<Route path={`/${Resource.Proxies.toLowerCase()}/detail/:id`} element={<ProxyDetail />} />

<Route path={`/${Resource.Discoveries.toLowerCase()}`} element={<DiscoveriesList />} />
<Route
path={`/${Resource.Discoveries.toLowerCase()}/list`}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { test, expect } from '../../../playwright/ct-test';
import { InstallationInstructions } from './InstallationInstructions';
import { withProviders } from 'utils/test-helpers';

type ClipboardSpy = { lastText: string; calls: number };

declare global {
interface Window {
__clipboardSpy?: ClipboardSpy;
}
}

test.describe('InstallationInstructions', () => {
test('should render with a single instruction string', async ({ mount }) => {
const component = await mount(
withProviders(<InstallationInstructions title="Test Installation" instructions="npm install package" />),
);

await expect(component.getByText('Test Installation')).toBeVisible();
await expect(component.getByText('npm install package')).toBeVisible();
});

test('should render with multiple instruction strings', async ({ mount }) => {
const instructions = ['npm install package', 'npm run build', 'npm start'];

const component = await mount(
withProviders(<InstallationInstructions title="Multi Step Installation" instructions={instructions} />),
);

await expect(component.getByText('Multi Step Installation')).toBeVisible();
await expect(component.getByText('npm install package')).toBeVisible();
await expect(component.getByText('npm run build')).toBeVisible();
await expect(component.getByText('npm start')).toBeVisible();
});

test('should apply custom id attribute', async ({ mount }) => {
const component = await mount(
withProviders(<InstallationInstructions title="ID Test" instructions="test" id="custom-installation-id" />),
);

await expect(component).toBeVisible();
await expect(component).toHaveAttribute('id', 'custom-installation-id');
});

test('should render title in header section', async ({ mount }) => {
const component = await mount(withProviders(<InstallationInstructions title="Header Title Test" instructions="instruction" />));

const title = component.getByText('Header Title Test');
await expect(title).toBeVisible();
await expect(title).toHaveClass(/text-lg/);
});

test('should join array instructions with newlines for clipboard', async ({ mount, page }) => {
const instructions = ['first command', 'second command', 'third command'];

await page.evaluate(() => {
window.__clipboardSpy = { lastText: '', calls: 0 };

Check warning on line 57 in src/components/InstallationInstructions/InstallationInstructions.spec.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytjD2wN1zZUhMfZd4n&open=AZytjD2wN1zZUhMfZd4n&pullRequest=1272
Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: async (text: string) => {
window.__clipboardSpy!.lastText = text;

Check warning on line 61 in src/components/InstallationInstructions/InstallationInstructions.spec.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytjD2wN1zZUhMfZd4o&open=AZytjD2wN1zZUhMfZd4o&pullRequest=1272
window.__clipboardSpy!.calls += 1;

Check warning on line 62 in src/components/InstallationInstructions/InstallationInstructions.spec.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytjD2wN1zZUhMfZd4p&open=AZytjD2wN1zZUhMfZd4p&pullRequest=1272
},
},
configurable: true,
});
});

const component = await mount(withProviders(<InstallationInstructions title="Clipboard Test" instructions={instructions} />));

const copyButton = component.getByTestId('copy-instructions-button');

await copyButton.click();

await expect.poll(async () => page.evaluate(() => window.__clipboardSpy?.calls ?? 0)).toBe(1);

Check warning on line 75 in src/components/InstallationInstructions/InstallationInstructions.spec.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytjD2wN1zZUhMfZd4q&open=AZytjD2wN1zZUhMfZd4q&pullRequest=1272
const clipboardState = await page.evaluate(() => window.__clipboardSpy);

Check warning on line 76 in src/components/InstallationInstructions/InstallationInstructions.spec.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytjD2wN1zZUhMfZd4r&open=AZytjD2wN1zZUhMfZd4r&pullRequest=1272
await expect(clipboardState!.lastText).toBe('first command\nsecond command\nthird command');

Check failure on line 77 in src/components/InstallationInstructions/InstallationInstructions.spec.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytjD2wN1zZUhMfZd4s&open=AZytjD2wN1zZUhMfZd4s&pullRequest=1272
});

test('should handle empty instructions array gracefully', async ({ mount }) => {
const component = await mount(withProviders(<InstallationInstructions title="Empty Instructions" instructions={[]} />));

await expect(component.getByText('Empty Instructions')).toBeVisible();
});

test('should handle very long instruction text', async ({ mount }) => {
const longInstruction = 'npm install ' + 'package-name-'.repeat(50);

const component = await mount(withProviders(<InstallationInstructions title="Long Text Test" instructions={longInstruction} />));

await expect(component.getByText(/npm install/)).toBeVisible();
});

test('should handle special characters in instructions', async ({ mount }) => {
const instructions = [
'export API_KEY="abc$123&test"',
'curl -X POST https://example.com/api?key=value',
'echo "Line with | pipe and > redirect"',
];

const component = await mount(
withProviders(<InstallationInstructions title="Special Characters Test" instructions={instructions} />),
);

await expect(component.getByText(/abc\$123&test/)).toBeVisible();
await expect(component.getByText(/\?key=value/)).toBeVisible();
await expect(component.getByText(/\| pipe and > redirect/)).toBeVisible();
});

test('should split single string by newline characters', async ({ mount }) => {
const instructions = 'npm install package\nnpm run build\nnpm start';

const component = await mount(withProviders(<InstallationInstructions title="Newline Split Test" instructions={instructions} />));

await expect(component.getByText('npm install package')).toBeVisible();
await expect(component.getByText('npm run build')).toBeVisible();
await expect(component.getByText('npm start')).toBeVisible();
});

test('should split single string with mixed newlines and preserve whitespace', async ({ mount }) => {
const instructions = 'function example() {\n console.log("hello");\n console.log("indented");\n}';

const component = await mount(
withProviders(<InstallationInstructions title="Mixed Newlines and Spaces Test" instructions={instructions} />),
);

await expect(component.getByText('function example() {')).toBeVisible();
await expect(component.getByText(/console\.log\("hello"\)/)).toBeVisible();
await expect(component.getByText(/console\.log\("indented"\)/)).toBeVisible();
await expect(component.getByText('}')).toBeVisible();
});

test('should preserve empty string after newline split', async ({ mount }) => {
const instructions = 'first line\n\nthird line';

const component = await mount(withProviders(<InstallationInstructions title="Empty Line Test" instructions={instructions} />));

await expect(component.getByText('first line')).toBeVisible();
await expect(component.getByText('third line')).toBeVisible();

// Check that 3 divs are rendered. The empty line should render with a non-breaking space.
await expect(component.getByTestId('instruction-line-0')).toBeVisible();
await expect(component.getByTestId('instruction-line-2')).toBeVisible();
const emptyLine = component.getByTestId('instruction-line-1');
await expect(emptyLine).toBeVisible();
await expect(emptyLine).toHaveText('\u00A0');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import cn from 'classnames';
import { Copy } from 'lucide-react';
import Button from 'components/Button';
import { useCopyToClipboard } from 'utils/common-hooks';

interface Props {
title: string;
instructions: string | string[];
className?: string;
id?: string;
'data-testid'?: string;
}

export const InstallationInstructions = ({ title, instructions, className, id, 'data-testid': dataTestId }: Props) => {
const copyToClipboard = useCopyToClipboard();

const instructionsText = Array.isArray(instructions) ? instructions.join('\n') : instructions;
// If instructions is a string, split it by newline characters
const instructionsAsArray = Array.isArray(instructions) ? instructions : instructions.split('\n');

const handleCopy = () => {
copyToClipboard(instructionsText, 'Installation instructions copied to clipboard.', 'Failed to copy to clipboard.');
};

const renderInstructions = () =>
instructionsAsArray.map((instruction, index) => (
<div key={index} className="font-mono text-xs whitespace-pre" data-testid={`instruction-line-${index}`}>

Check warning on line 28 in src/components/InstallationInstructions/InstallationInstructions.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=CZERTAINLY_CZERTAINLY-FE-Administrator&issues=AZytjD6eN1zZUhMfZd4t&open=AZytjD6eN1zZUhMfZd4t&pullRequest=1272
{instruction || '\u00A0'}
</div>
));

return (
<section
id={id}
data-testid={dataTestId}
className={cn('border border-[#1C2740] bg-[#0E1728] rounded-xl overflow-hidden', className)}
>
<div className="flex items-center justify-between px-10 py-5 border-b border-[#1C2740] text-[#A8A8A8]">
<h5 className="text-lg">{title}</h5>
<Button
variant="transparent"
color="secondary"
onClick={handleCopy}
title="Copy to clipboard"
data-testid="copy-instructions-button"
>
<Copy size={16} />
</Button>
</div>

<div className="px-10 py-5 flex flex-col gap-1.5 bg-[#0B1220] text-[#C0CAF5]">{renderInstructions()}</div>
</section>
);
};
9 changes: 9 additions & 0 deletions src/components/Layout/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
FileJson2,
Split,
ArrowRightToLine,
Network,
FileLock2,
} from 'lucide-react';
import Button from 'components/Button';
Expand Down Expand Up @@ -140,6 +141,14 @@ const menuItemMappings: MenuItemMapping[] = [
headerLink: '/cboms',
requiredResources: [Resource.Cboms],
},
{
_key: '/proxies',
icon: <Network size={16} strokeWidth={1.5} />,
header: 'Proxies',
headerLink: '/proxies',
requiredResources: [Resource.Proxies],
},

{
_key: 'accessControl',
icon: <CircleUser size={16} strokeWidth={1.5} />,
Expand Down
Loading
Loading