Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,8 @@
"maxChars": "Max length is {{maxLength}} characters.",
"userExists": "User with this name already exists!",
"atLeastOneUser": "You need to have at least one member assigned.",
"notValidChargingTargetFormat": "Use lowercase letters a-f, numbers 0-9, and hyphens (-) in the format: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
"notValidChargingTargetFormat": "Use lowercase letters a-f, numbers 0-9, and hyphens (-) in the format: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"urlFormat": "Must be a valid HTTPS URL"
},
"common": {
"all": "All",
Expand Down Expand Up @@ -510,5 +511,17 @@
"addMembersButton0": "Add members",
"addMembersButton1": "Add member",
"addMembersButtonN": "Add {{count}} members"
},
"CreateGitRepositoryDialog": {
"dialogTitle": "Create Git Repository",
"metadataTitle": "Metadata",
"urlTitle": "URL",
"secretRefTitle": "SecretRef",
"nameTitle": "Name",
"intervalTitle": "Interval",
"specTitle": "Spec",
"branchTitle": "Branch",
"gitRepositoryCreated": "Git Repository created successfully",
"gitRepositoryCreationFailed": "Failed to create Git Repository: {{error}}"
}
}
57 changes: 33 additions & 24 deletions src/components/ControlPlane/GitRepositories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import ConfiguredAnalyticstable from '../Shared/ConfiguredAnalyticsTable.tsx';
import { useApiResource } from '../../lib/api/useApiResource';
import { FluxRequest } from '../../lib/api/types/flux/listGitRepo';
import { useTranslation } from 'react-i18next';
import { formatDateAsTimeAgo } from '../../utils/i18n/timeAgo.ts';

import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
import { Fragment, useCallback, useContext, useMemo, useRef, useState } from 'react';
import {
AnalyticalTableColumnDefinition,
Panel,
Expand All @@ -7,15 +14,8 @@ import {
ToolbarSpacer,
Button,
} from '@ui5/webcomponents-react';
import '@ui5/webcomponents-icons/dist/add';
import IllustratedError from '../Shared/IllustratedError.tsx';
import { useApiResource } from '../../lib/api/useApiResource';
import { FluxRequest } from '../../lib/api/types/flux/listGitRepo';
import { useTranslation } from 'react-i18next';
import { formatDateAsTimeAgo } from '../../utils/i18n/timeAgo.ts';

import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
import { Fragment, useCallback, useContext, useMemo, useRef } from 'react';
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
import { useSplitter } from '../Splitter/SplitterContext.tsx';
Expand All @@ -27,6 +27,8 @@ import { ActionsMenu, type ActionItem } from './ActionsMenu';

import { ApiConfigContext } from '../Shared/k8s';
import { useHasMcpAdminRights } from '../../spaces/mcp/auth/useHasMcpAdminRights.ts';
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
import { CreateGitRepositoryDialog } from '../Dialogs/CreateGitRepositoryDialog.tsx';

export type GitRepoItem = GitReposResponse['items'][0] & {
apiVersion?: string;
Expand All @@ -39,6 +41,7 @@ export function GitRepositories() {
const { openInAsideWithApiConfig } = useSplitter();
const errorDialogRef = useRef<ErrorDialogHandle>(null);
const handlePatch = useHandleResourcePatch(errorDialogRef);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);

type FluxRow = {
name: string;
Expand Down Expand Up @@ -186,21 +189,28 @@ export function GitRepositories() {
}) ?? [];

return (
<Panel
fixed
header={
<Toolbar>
<Title>{t('common.resourcesCount', { count: rows.length })}</Title>
<YamlViewButton variant="resource" resource={data as unknown as Resource} />
<ToolbarSpacer />
</Toolbar>
}
>
<>
<ConfiguredAnalyticstable columns={columns} isLoading={isLoading} data={rows} />
<ErrorDialog ref={errorDialogRef} />
</>
</Panel>
<>
<Panel
fixed
header={
<Toolbar>
<Title>{t('common.resourcesCount', { count: rows.length })}</Title>
<YamlViewButton variant="resource" resource={data as unknown as Resource} />
<ToolbarSpacer />
<Button icon="add" onClick={() => setIsCreateDialogOpen(true)}>
{t('buttons.create')}
</Button>
</Toolbar>
}
>
<>
<ConfiguredAnalyticstable columns={columns} isLoading={isLoading} data={rows} />
<ErrorDialog ref={errorDialogRef} />
</>
</Panel>

<CreateGitRepositoryDialog isOpen={isCreateDialogOpen} onClose={() => setIsCreateDialogOpen(false)} />
</>
);
}

Expand All @@ -211,7 +221,6 @@ function shortenCommitHash(commitHash: string): string {
if (match && match[2]) {
return `${match[1]}@${match[2].slice(0, 7)}`;
}

//example output : master@b3396ad
return commitHash;
}
193 changes: 193 additions & 0 deletions src/components/Dialogs/CreateGitRepositoryDialog.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { CreateGitRepositoryDialog } from './CreateGitRepositoryDialog';
import { CreateGitRepositoryParams } from '../../hooks/useCreateGitRepository';

describe('CreateGitRepositoryDialog', () => {
let capturedData: CreateGitRepositoryParams | null = null;

const fakeUseCreateGitRepository = () => ({
createGitRepository: async (data: CreateGitRepositoryParams): Promise<void> => {
capturedData = data;
},
isLoading: false,
});

beforeEach(() => {
capturedData = null;
});

it('creates a git repository with valid data', () => {
const onClose = cy.stub();

cy.mount(
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
);

const expectedPayload = {
namespace: 'default',
name: 'test-repo',
interval: '5m0s',
url: 'https://github.com/test/repo',
branch: 'develop',
secretRef: '',
};

// Fill in the form
cy.get('[name="name"]').typeIntoUi5Input('test-repo');
cy.get('[name="interval"]').find('input').clear().type('5m0s');

cy.get('[name="url"]').typeIntoUi5Input('https://github.com/test/repo');
cy.get('[name="branch"]').find('input').clear().type('develop');

// Submit the form
cy.get('ui5-button').contains('Create').click();

// Verify the hook was called with correct data
cy.then(() => cy.wrap(capturedData).deepEqualJson(expectedPayload));

// Dialog should close on success
cy.wrap(onClose).should('have.been.called');
});

it('includes secretRef when provided', () => {
const onClose = cy.stub();

cy.mount(
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
);

const expectedPayload = {
namespace: 'default',
name: 'test-repo',
interval: '1m0s',
url: 'https://github.com/test/repo',
branch: 'main',
secretRef: 'my-git-secret',
};

// Fill in the form
cy.get('[name="name"]').typeIntoUi5Input('test-repo');
cy.get('[name="url"]').typeIntoUi5Input('https://github.com/test/repo');
cy.get('[name="secretRef"]').typeIntoUi5Input('my-git-secret');

// Submit the form
cy.get('ui5-button').contains('Create').click();

// Verify the hook was called with correct data
cy.then(() => cy.wrap(capturedData).deepEqualJson(expectedPayload));

// Dialog should close on success
cy.wrap(onClose).should('have.been.called');
});

it('validates required fields', () => {
const onClose = cy.stub();

cy.mount(
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
);

// Try to submit without filling required fields
cy.get('ui5-button').contains('Create').click();

// Should show validation errors
cy.get('[name="name"]').should('have.attr', 'value-state', 'Negative');
cy.contains('This field is required').should('exist');

// Dialog should not close
cy.wrap(onClose).should('not.have.been.called');
});

it('validates URL format', () => {
const onClose = cy.stub();

cy.mount(
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
);

cy.get('[name="name"]').typeIntoUi5Input('test-repo');
cy.get('[name="interval"]').find('input').clear().type('1m0s');
cy.get('[name="branch"]').find('input').clear().type('main');

// Test 1: Invalid string
cy.get('[name="url"]').find('input').clear().type('not-a-valid-url');
cy.get('ui5-button').contains('Create').click();
cy.get('[name="url"]').should('have.attr', 'value-state', 'Negative');
cy.contains('Must be a valid HTTPS URL').should('exist');

// Test 2: HTTP protocol (should fail if we require HTTPS)
cy.get('[name="url"]').find('input').clear().type('http://github.com/test/repo');
cy.get('ui5-button').contains('Create').click();
cy.get('[name="url"]').should('have.attr', 'value-state', 'Negative');
cy.contains('Must be a valid HTTPS URL').should('exist');

// Test 3: Valid HTTPS URL (should pass)
cy.get('[name="url"]').find('input').clear().type('https://github.com/test/repo');
cy.get('ui5-button').contains('Create').click();

// Dialog should close on success
cy.wrap(onClose).should('have.been.called');
});

it('closes dialog when cancel is clicked', () => {
const onClose = cy.stub();

cy.mount(
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
);

// Fill in some data
cy.get('[name="name"]').typeIntoUi5Input('test-repo');

// Click cancel
cy.get('ui5-button').contains('Cancel').click();

// Dialog should close without calling onSuccess
cy.wrap(onClose).should('have.been.called');
});

it('uses default values for interval and branch', () => {
const onClose = cy.stub();

cy.mount(
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
);

// Check default values
cy.get('[name="interval"]').find('input').should('have.value', '1m0s');
cy.get('[name="branch"]').find('input').should('have.value', 'main');
});

it('should not close dialog when creation fails', () => {
const failingUseCreateGitRepository = () => ({
createGitRepository: async (): Promise<void> => {
throw new Error('Creation failed');
},
isLoading: false,
});

const onClose = cy.stub();

cy.mount(
<CreateGitRepositoryDialog
isOpen={true}
useCreateGitRepository={failingUseCreateGitRepository}
onClose={onClose}
/>,
);

// Fill in the form
cy.get('[name="name"]').typeIntoUi5Input('test-repo');
cy.get('[name="interval"]').find('input').clear().type('1m0s');
cy.get('[name="url"]').find('input').type('https://github.com/test/repo');
cy.get('[name="branch"]').find('input').clear().type('main');

// Submit the form
cy.get('ui5-button').contains('Create').click();

// Dialog should NOT close on failure
cy.wrap(onClose).should('not.have.been.called');

// Dialog should still be visible
cy.contains('Create Git Repository').should('be.visible');
});
});
41 changes: 41 additions & 0 deletions src/components/Dialogs/CreateGitRepositoryDialog.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
padding: 1rem;
align-items: center;
}

.gridColumnLabel {
justify-self: end;
}

.inputField {
width: 25rem;
}

.sectionHeader {
grid-column: 1 / -1;
font-weight: bold;
font-size: var(--sapFontHeader6Size);
padding-top: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--sapGroup_Title_BorderColor);
margin-bottom: 0.5rem;
}

.sectionHeader:first-child {
padding-top: 0;
}

.form {
width: 30rem;
}

.formField {
margin-bottom: 1.25rem;
}

.input {
width: 100%;
}
Loading
Loading