Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
155 changes: 99 additions & 56 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 15 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 @@ -507,5 +508,18 @@
"addMembersButton0": "Add members",
"addMembersButton1": "Add member",
"addMembersButtonN": "Add {{count}} members"
},
"CreateGitRepositoryDialog": {
"dialogTitle": "Create Git Repository'",
"metadataTitle": "Metadata",
"urlTitle": "URL",
"secretRefOptionalTitle": "SecretRef (Optional)",
"secretRefTitle": "SecretRef",
"nameTitle": "Name",
"intervalTitle": "Interval",
"specTitle": "Spec",
"branchTitle": "Branch",
"gitRepositoryCreated": "Git Repository created successfully",
"gitRepositoryCreationFailed": "Failed to create Git Repository: {{error}}"
}
}
59 changes: 39 additions & 20 deletions src/components/ControlPlane/GitRepositories.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import ConfiguredAnalyticstable from '../Shared/ConfiguredAnalyticsTable.tsx';
import { AnalyticalTableColumnDefinition, Panel, Title, Toolbar, ToolbarSpacer } from '@ui5/webcomponents-react';
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, useMemo, useRef } from 'react';
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
import { Fragment, useCallback, useMemo, useRef, useState } from 'react';
import {
AnalyticalTableColumnDefinition,
Panel,
Title,
Toolbar,
ToolbarSpacer,
Button,
} from '@ui5/webcomponents-react';
import '@ui5/webcomponents-icons/dist/add';
import IllustratedError from '../Shared/IllustratedError.tsx';
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
import { useSplitter } from '../Splitter/SplitterContext.tsx';
Expand All @@ -17,6 +24,8 @@ import { useHandleResourcePatch } from '../../hooks/useHandleResourcePatch.ts';
import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
import type { GitReposResponse } from '../../lib/api/types/flux/listGitRepo';
import { ActionsMenu, type ActionItem } from './ActionsMenu';
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
import { CreateGitRepositoryDialog } from '../Dialogs/CreateGitRepositoryDialog.tsx';

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

type FluxRow = {
name: string;
Expand Down Expand Up @@ -152,21 +162,31 @@ 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>
<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}
namespace={rows[0]?.item?.metadata?.namespace || 'default'}
onClose={() => setIsCreateDialogOpen(false)}
/>
</>
);
}

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

//example output : master@b3396ad
return commitHash;
}
233 changes: 233 additions & 0 deletions src/components/Dialogs/CreateGitRepositoryDialog.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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();
const onSuccess = cy.stub();

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

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

// Fill in the form
cy.get('#name').find('input').type('test-repo');
cy.get('#interval').find('input').clear().type('5m0s');
cy.get('#url').find('input').type('https://github.com/test/repo');
cy.get('#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');
cy.wrap(onSuccess).should('have.been.called');
});

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

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

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

// Fill in the form
cy.get('#name').find('input').type('test-repo');
cy.get('#url').find('input').type('https://github.com/test/repo');
cy.get('#secretRef').find('input').type('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');
cy.wrap(onSuccess).should('have.been.called');
});

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

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

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

// Should show validation errors
cy.get('#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');
cy.wrap(onSuccess).should('not.have.been.called');
});

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

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

cy.get('#name').find('input').type('test-repo');
cy.get('#interval').find('input').clear().type('1m0s');
cy.get('#url').find('input').type('not-a-valid-url');
cy.get('#branch').find('input').clear().type('main');

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

// Should show validation error
cy.get('#url').should('have.attr', 'value-state', 'Negative');
cy.contains('Must be a valid HTTPS URL').should('exist');

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

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

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

// Fill in some data
cy.get('#name').find('input').type('test-repo');

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

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

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

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

// Check default values
cy.get('#interval').find('input').should('have.value', '1m0s');
cy.get('#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();
const onSuccess = cy.stub();

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

// Fill in the form
cy.get('#name').find('input').type('test-repo');
cy.get('#interval').find('input').clear().type('1m0s');
cy.get('#url').find('input').type('https://github.com/test/repo');
cy.get('#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');
cy.wrap(onSuccess).should('not.have.been.called');

// Dialog should still be visible
cy.contains('Create Git Repository').should('be.visible');
});
});
Loading
Loading