Skip to content

Commit c2efe6d

Browse files
feat: adding git repo (#374)
1 parent 56f257b commit c2efe6d

File tree

8 files changed

+802
-25
lines changed

8 files changed

+802
-25
lines changed

public/locales/en.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,8 @@
364364
"maxChars": "Max length is {{maxLength}} characters.",
365365
"userExists": "User with this name already exists!",
366366
"atLeastOneUser": "You need to have at least one member assigned.",
367-
"notValidChargingTargetFormat": "Use lowercase letters a-f, numbers 0-9, and hyphens (-) in the format: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
367+
"notValidChargingTargetFormat": "Use lowercase letters a-f, numbers 0-9, and hyphens (-) in the format: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
368+
"urlFormat": "Must be a valid HTTPS URL"
368369
},
369370
"common": {
370371
"all": "All",
@@ -510,5 +511,17 @@
510511
"addMembersButton0": "Add members",
511512
"addMembersButton1": "Add member",
512513
"addMembersButtonN": "Add {{count}} members"
514+
},
515+
"CreateGitRepositoryDialog": {
516+
"dialogTitle": "Create Git Repository",
517+
"metadataTitle": "Metadata",
518+
"urlTitle": "URL",
519+
"secretRefTitle": "SecretRef",
520+
"nameTitle": "Name",
521+
"intervalTitle": "Interval",
522+
"specTitle": "Spec",
523+
"branchTitle": "Branch",
524+
"gitRepositoryCreated": "Git Repository created successfully",
525+
"gitRepositoryCreationFailed": "Failed to create Git Repository: {{error}}"
513526
}
514527
}

src/components/ControlPlane/GitRepositories.tsx

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import ConfiguredAnalyticstable from '../Shared/ConfiguredAnalyticsTable.tsx';
2+
import { useApiResource } from '../../lib/api/useApiResource';
3+
import { FluxRequest } from '../../lib/api/types/flux/listGitRepo';
4+
import { useTranslation } from 'react-i18next';
5+
import { formatDateAsTimeAgo } from '../../utils/i18n/timeAgo.ts';
6+
7+
import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
8+
import { Fragment, useCallback, useContext, useMemo, useRef, useState } from 'react';
29
import {
310
AnalyticalTableColumnDefinition,
411
Panel,
@@ -7,15 +14,8 @@ import {
714
ToolbarSpacer,
815
Button,
916
} from '@ui5/webcomponents-react';
17+
import '@ui5/webcomponents-icons/dist/add';
1018
import IllustratedError from '../Shared/IllustratedError.tsx';
11-
import { useApiResource } from '../../lib/api/useApiResource';
12-
import { FluxRequest } from '../../lib/api/types/flux/listGitRepo';
13-
import { useTranslation } from 'react-i18next';
14-
import { formatDateAsTimeAgo } from '../../utils/i18n/timeAgo.ts';
15-
16-
import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
17-
import { Fragment, useCallback, useContext, useMemo, useRef } from 'react';
18-
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
1919
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
2020
import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
2121
import { useSplitter } from '../Splitter/SplitterContext.tsx';
@@ -27,6 +27,8 @@ import { ActionsMenu, type ActionItem } from './ActionsMenu';
2727

2828
import { ApiConfigContext } from '../Shared/k8s';
2929
import { useHasMcpAdminRights } from '../../spaces/mcp/auth/useHasMcpAdminRights.ts';
30+
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
31+
import { CreateGitRepositoryDialog } from '../Dialogs/CreateGitRepositoryDialog.tsx';
3032

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

4346
type FluxRow = {
4447
name: string;
@@ -186,21 +189,28 @@ export function GitRepositories() {
186189
}) ?? [];
187190

188191
return (
189-
<Panel
190-
fixed
191-
header={
192-
<Toolbar>
193-
<Title>{t('common.resourcesCount', { count: rows.length })}</Title>
194-
<YamlViewButton variant="resource" resource={data as unknown as Resource} />
195-
<ToolbarSpacer />
196-
</Toolbar>
197-
}
198-
>
199-
<>
200-
<ConfiguredAnalyticstable columns={columns} isLoading={isLoading} data={rows} />
201-
<ErrorDialog ref={errorDialogRef} />
202-
</>
203-
</Panel>
192+
<>
193+
<Panel
194+
fixed
195+
header={
196+
<Toolbar>
197+
<Title>{t('common.resourcesCount', { count: rows.length })}</Title>
198+
<YamlViewButton variant="resource" resource={data as unknown as Resource} />
199+
<ToolbarSpacer />
200+
<Button icon="add" onClick={() => setIsCreateDialogOpen(true)}>
201+
{t('buttons.create')}
202+
</Button>
203+
</Toolbar>
204+
}
205+
>
206+
<>
207+
<ConfiguredAnalyticstable columns={columns} isLoading={isLoading} data={rows} />
208+
<ErrorDialog ref={errorDialogRef} />
209+
</>
210+
</Panel>
211+
212+
<CreateGitRepositoryDialog isOpen={isCreateDialogOpen} onClose={() => setIsCreateDialogOpen(false)} />
213+
</>
204214
);
205215
}
206216

@@ -211,7 +221,6 @@ function shortenCommitHash(commitHash: string): string {
211221
if (match && match[2]) {
212222
return `${match[1]}@${match[2].slice(0, 7)}`;
213223
}
214-
215224
//example output : master@b3396ad
216225
return commitHash;
217226
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { CreateGitRepositoryDialog } from './CreateGitRepositoryDialog';
2+
import { CreateGitRepositoryParams } from '../../hooks/useCreateGitRepository';
3+
4+
describe('CreateGitRepositoryDialog', () => {
5+
let capturedData: CreateGitRepositoryParams | null = null;
6+
7+
const fakeUseCreateGitRepository = () => ({
8+
createGitRepository: async (data: CreateGitRepositoryParams): Promise<void> => {
9+
capturedData = data;
10+
},
11+
isLoading: false,
12+
});
13+
14+
beforeEach(() => {
15+
capturedData = null;
16+
});
17+
18+
it('creates a git repository with valid data', () => {
19+
const onClose = cy.stub();
20+
21+
cy.mount(
22+
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
23+
);
24+
25+
const expectedPayload = {
26+
namespace: 'default',
27+
name: 'test-repo',
28+
interval: '5m0s',
29+
url: 'https://github.com/test/repo',
30+
branch: 'develop',
31+
secretRef: '',
32+
};
33+
34+
// Fill in the form
35+
cy.get('[name="name"]').typeIntoUi5Input('test-repo');
36+
cy.get('[name="interval"]').find('input').clear().type('5m0s');
37+
38+
cy.get('[name="url"]').typeIntoUi5Input('https://github.com/test/repo');
39+
cy.get('[name="branch"]').find('input').clear().type('develop');
40+
41+
// Submit the form
42+
cy.get('ui5-button').contains('Create').click();
43+
44+
// Verify the hook was called with correct data
45+
cy.then(() => cy.wrap(capturedData).deepEqualJson(expectedPayload));
46+
47+
// Dialog should close on success
48+
cy.wrap(onClose).should('have.been.called');
49+
});
50+
51+
it('includes secretRef when provided', () => {
52+
const onClose = cy.stub();
53+
54+
cy.mount(
55+
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
56+
);
57+
58+
const expectedPayload = {
59+
namespace: 'default',
60+
name: 'test-repo',
61+
interval: '1m0s',
62+
url: 'https://github.com/test/repo',
63+
branch: 'main',
64+
secretRef: 'my-git-secret',
65+
};
66+
67+
// Fill in the form
68+
cy.get('[name="name"]').typeIntoUi5Input('test-repo');
69+
cy.get('[name="url"]').typeIntoUi5Input('https://github.com/test/repo');
70+
cy.get('[name="secretRef"]').typeIntoUi5Input('my-git-secret');
71+
72+
// Submit the form
73+
cy.get('ui5-button').contains('Create').click();
74+
75+
// Verify the hook was called with correct data
76+
cy.then(() => cy.wrap(capturedData).deepEqualJson(expectedPayload));
77+
78+
// Dialog should close on success
79+
cy.wrap(onClose).should('have.been.called');
80+
});
81+
82+
it('validates required fields', () => {
83+
const onClose = cy.stub();
84+
85+
cy.mount(
86+
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
87+
);
88+
89+
// Try to submit without filling required fields
90+
cy.get('ui5-button').contains('Create').click();
91+
92+
// Should show validation errors
93+
cy.get('[name="name"]').should('have.attr', 'value-state', 'Negative');
94+
cy.contains('This field is required').should('exist');
95+
96+
// Dialog should not close
97+
cy.wrap(onClose).should('not.have.been.called');
98+
});
99+
100+
it('validates URL format', () => {
101+
const onClose = cy.stub();
102+
103+
cy.mount(
104+
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
105+
);
106+
107+
cy.get('[name="name"]').typeIntoUi5Input('test-repo');
108+
cy.get('[name="interval"]').find('input').clear().type('1m0s');
109+
cy.get('[name="branch"]').find('input').clear().type('main');
110+
111+
// Test 1: Invalid string
112+
cy.get('[name="url"]').find('input').clear().type('not-a-valid-url');
113+
cy.get('ui5-button').contains('Create').click();
114+
cy.get('[name="url"]').should('have.attr', 'value-state', 'Negative');
115+
cy.contains('Must be a valid HTTPS URL').should('exist');
116+
117+
// Test 2: HTTP protocol (should fail if we require HTTPS)
118+
cy.get('[name="url"]').find('input').clear().type('http://github.com/test/repo');
119+
cy.get('ui5-button').contains('Create').click();
120+
cy.get('[name="url"]').should('have.attr', 'value-state', 'Negative');
121+
cy.contains('Must be a valid HTTPS URL').should('exist');
122+
123+
// Test 3: Valid HTTPS URL (should pass)
124+
cy.get('[name="url"]').find('input').clear().type('https://github.com/test/repo');
125+
cy.get('ui5-button').contains('Create').click();
126+
127+
// Dialog should close on success
128+
cy.wrap(onClose).should('have.been.called');
129+
});
130+
131+
it('closes dialog when cancel is clicked', () => {
132+
const onClose = cy.stub();
133+
134+
cy.mount(
135+
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
136+
);
137+
138+
// Fill in some data
139+
cy.get('[name="name"]').typeIntoUi5Input('test-repo');
140+
141+
// Click cancel
142+
cy.get('ui5-button').contains('Cancel').click();
143+
144+
// Dialog should close without calling onSuccess
145+
cy.wrap(onClose).should('have.been.called');
146+
});
147+
148+
it('uses default values for interval and branch', () => {
149+
const onClose = cy.stub();
150+
151+
cy.mount(
152+
<CreateGitRepositoryDialog isOpen={true} useCreateGitRepository={fakeUseCreateGitRepository} onClose={onClose} />,
153+
);
154+
155+
// Check default values
156+
cy.get('[name="interval"]').find('input').should('have.value', '1m0s');
157+
cy.get('[name="branch"]').find('input').should('have.value', 'main');
158+
});
159+
160+
it('should not close dialog when creation fails', () => {
161+
const failingUseCreateGitRepository = () => ({
162+
createGitRepository: async (): Promise<void> => {
163+
throw new Error('Creation failed');
164+
},
165+
isLoading: false,
166+
});
167+
168+
const onClose = cy.stub();
169+
170+
cy.mount(
171+
<CreateGitRepositoryDialog
172+
isOpen={true}
173+
useCreateGitRepository={failingUseCreateGitRepository}
174+
onClose={onClose}
175+
/>,
176+
);
177+
178+
// Fill in the form
179+
cy.get('[name="name"]').typeIntoUi5Input('test-repo');
180+
cy.get('[name="interval"]').find('input').clear().type('1m0s');
181+
cy.get('[name="url"]').find('input').type('https://github.com/test/repo');
182+
cy.get('[name="branch"]').find('input').clear().type('main');
183+
184+
// Submit the form
185+
cy.get('ui5-button').contains('Create').click();
186+
187+
// Dialog should NOT close on failure
188+
cy.wrap(onClose).should('not.have.been.called');
189+
190+
// Dialog should still be visible
191+
cy.contains('Create Git Repository').should('be.visible');
192+
});
193+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.grid {
2+
display: grid;
3+
grid-template-columns: auto 1fr;
4+
gap: 1rem;
5+
padding: 1rem;
6+
align-items: center;
7+
}
8+
9+
.gridColumnLabel {
10+
justify-self: end;
11+
}
12+
13+
.inputField {
14+
width: 25rem;
15+
}
16+
17+
.sectionHeader {
18+
grid-column: 1 / -1;
19+
font-weight: bold;
20+
font-size: var(--sapFontHeader6Size);
21+
padding-top: 0.5rem;
22+
padding-bottom: 0.5rem;
23+
border-bottom: 1px solid var(--sapGroup_Title_BorderColor);
24+
margin-bottom: 0.5rem;
25+
}
26+
27+
.sectionHeader:first-child {
28+
padding-top: 0;
29+
}
30+
31+
.form {
32+
width: 30rem;
33+
}
34+
35+
.formField {
36+
margin-bottom: 1.25rem;
37+
}
38+
39+
.input {
40+
width: 100%;
41+
}

0 commit comments

Comments
 (0)