Skip to content
This repository was archived by the owner on Nov 7, 2025. It is now read-only.

Commit c0d81c2

Browse files
committed
feat: configure git providers through the ui
1 parent 1a47f66 commit c0d81c2

10 files changed

Lines changed: 1083 additions & 0 deletions

File tree

src/layouts/AdminLayout/AdminLayout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ export default function AdminLayout({ children }: Props) {
104104
isActive: pathname.includes("/admin/proxies"),
105105
}}
106106
/>
107+
<MenuLink
108+
item={{
109+
path: "/admin/git",
110+
text: "Git",
111+
isActive: pathname.includes("/admin/git"),
112+
}}
113+
/>
107114
{isCloud && (
108115
<MenuLink
109116
item={{
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2+
import {
3+
render,
4+
waitFor,
5+
fireEvent,
6+
type RenderResult,
7+
} from "@testing-library/react";
8+
import nock from "nock";
9+
import BitbucketModal from "./BitbucketModal";
10+
import { GitDetails } from "./types.d";
11+
12+
const apiDomain = process.env.API_DOMAIN || "";
13+
14+
describe("~/pages/admin/Git/BitbucketModal.tsx", () => {
15+
let wrapper: RenderResult;
16+
const mockCloseModal = vi.fn();
17+
const mockOnSuccess = vi.fn();
18+
19+
const findClientID = () =>
20+
wrapper.getByLabelText(/Client ID/) as HTMLInputElement;
21+
const findClientSecret = () =>
22+
wrapper.getByLabelText(/Client secret/) as HTMLInputElement;
23+
const findDeployKey = () =>
24+
wrapper.getByLabelText(/Deploy key/) as HTMLInputElement;
25+
const findCancelButton = () =>
26+
wrapper.getByRole("button", { name: /Cancel/ }) as HTMLButtonElement;
27+
const findConfigureButton = () =>
28+
wrapper.getByRole("button", { name: /Configure/ }) as HTMLButtonElement;
29+
30+
describe("without existing details", () => {
31+
beforeEach(() => {
32+
nock.cleanAll();
33+
vi.clearAllMocks();
34+
wrapper = render(
35+
<BitbucketModal closeModal={mockCloseModal} onSuccess={mockOnSuccess} />
36+
);
37+
});
38+
39+
afterEach(() => {
40+
nock.cleanAll();
41+
});
42+
43+
it("should render modal with correct title and form fields", async () => {
44+
await waitFor(() => {
45+
wrapper.getByText("Configure Bitbucket");
46+
wrapper.getByText("Client ID");
47+
wrapper.getByText("Client secret");
48+
wrapper.getByText("Deploy key");
49+
wrapper.getByRole("button", { name: "Cancel" });
50+
wrapper.getByRole("button", { name: "Configure" });
51+
});
52+
});
53+
});
54+
55+
describe("with existing details", () => {
56+
const existingDetails: GitDetails = {
57+
bitbucket: {
58+
clientId: "existing-client-id",
59+
hasDeployKey: true,
60+
hasClientSecret: true,
61+
},
62+
};
63+
64+
beforeEach(() => {
65+
nock.cleanAll();
66+
vi.clearAllMocks();
67+
wrapper = render(
68+
<BitbucketModal
69+
closeModal={mockCloseModal}
70+
onSuccess={mockOnSuccess}
71+
details={existingDetails}
72+
/>
73+
);
74+
});
75+
76+
afterEach(() => {
77+
nock.cleanAll();
78+
});
79+
80+
it("should populate fields with existing details", () => {
81+
expect(findClientID().value).toBe("existing-client-id");
82+
expect(findClientSecret().placeholder).toBe("***************");
83+
expect(findDeployKey().placeholder).toBe("Enter deploy key");
84+
});
85+
});
86+
87+
describe("form submission", () => {
88+
beforeEach(() => {
89+
nock.cleanAll();
90+
vi.clearAllMocks();
91+
wrapper = render(
92+
<BitbucketModal closeModal={mockCloseModal} onSuccess={mockOnSuccess} />
93+
);
94+
});
95+
96+
afterEach(() => {
97+
nock.cleanAll();
98+
});
99+
100+
it("should call closeModal when cancel button is clicked", () => {
101+
fireEvent.click(findCancelButton());
102+
expect(mockCloseModal).toHaveBeenCalledTimes(1);
103+
});
104+
105+
it("should successfully submit configuration", async () => {
106+
fireEvent.change(findClientID(), { target: { value: "new-client-id" } });
107+
fireEvent.change(findClientSecret(), {
108+
target: { value: "new-client-secret" },
109+
});
110+
fireEvent.change(findDeployKey(), {
111+
target: { value: "new-deploy-key" },
112+
});
113+
114+
const scope = nock(apiDomain)
115+
.post("/admin/git/configure", {
116+
provider: "bitbucket",
117+
clientId: "new-client-id",
118+
clientSecret: "new-client-secret",
119+
deployKey: "new-deploy-key",
120+
})
121+
.reply(200, { success: true });
122+
123+
fireEvent.click(findConfigureButton());
124+
125+
await waitFor(() => {
126+
expect(scope.isDone()).toBe(true);
127+
expect(mockOnSuccess).toHaveBeenCalledTimes(1);
128+
});
129+
});
130+
});
131+
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { useState } from "react";
2+
import Box from "@mui/material/Box";
3+
import TextField from "@mui/material/TextField";
4+
import Button from "@mui/material/Button";
5+
import Modal from "~/components/Modal";
6+
import Card from "~/components/Card";
7+
import CardHeader from "~/components/CardHeader";
8+
import CardFooter from "~/components/CardFooter";
9+
import PasswordField from "~/components/PasswordField";
10+
import api from "~/utils/api/Api";
11+
import { GitDetails } from "./types.d";
12+
13+
interface Props {
14+
details?: GitDetails;
15+
closeModal: () => void;
16+
onSuccess: (message: string) => void;
17+
}
18+
19+
export default function BitbucketModal({
20+
closeModal,
21+
onSuccess,
22+
details,
23+
}: Props) {
24+
const bb = details?.bitbucket;
25+
const [error, setError] = useState<string>();
26+
const [loading, setLoading] = useState(false);
27+
const [clientId, setClientId] = useState(bb?.clientId || "");
28+
const [deployKey, setDeployKey] = useState("");
29+
const [clientSecret, setClientSecret] = useState("");
30+
31+
return (
32+
<Modal open onClose={closeModal} maxWidth="md">
33+
<Card
34+
component="form"
35+
error={error}
36+
onSubmit={e => {
37+
e.preventDefault();
38+
39+
if (!clientId.trim()) {
40+
setError("Client ID is required");
41+
return;
42+
}
43+
44+
if (!clientSecret.trim()) {
45+
setError("Client Secret is required");
46+
return;
47+
}
48+
49+
setLoading(true);
50+
51+
const payload = {
52+
provider: "bitbucket",
53+
clientId,
54+
clientSecret,
55+
...(deployKey.trim() && { deployKey }),
56+
};
57+
58+
api
59+
.post("/admin/git/configure", payload)
60+
.then(() => {
61+
onSuccess("Bitbucket configuration saved successfully");
62+
})
63+
.catch(res => {
64+
if (res?.error) {
65+
setError(res.error);
66+
} else {
67+
setError("Something went wrong while configuring Bitbucket");
68+
}
69+
})
70+
.finally(() => {
71+
setLoading(false);
72+
});
73+
}}
74+
>
75+
<CardHeader
76+
title="Configure Bitbucket"
77+
subtitle="Configure your Bitbucket application to enable authentication and access private repositories"
78+
/>
79+
80+
<Box sx={{ mb: 4 }}>
81+
<TextField
82+
label="Client ID"
83+
placeholder="Enter Bitbucket Client ID"
84+
value={clientId}
85+
onChange={e => setClientId(e.target.value)}
86+
fullWidth
87+
required
88+
variant="filled"
89+
sx={{ mb: 3 }}
90+
slotProps={{
91+
inputLabel: {
92+
shrink: true,
93+
},
94+
}}
95+
/>
96+
97+
<TextField
98+
label="Client secret"
99+
placeholder={
100+
bb?.hasClientSecret
101+
? "***************"
102+
: "The client secret of your Bitbucket application"
103+
}
104+
type="password"
105+
value={clientSecret}
106+
onChange={e => setClientSecret(e.target.value)}
107+
fullWidth
108+
required
109+
variant="filled"
110+
sx={{ mb: 3 }}
111+
slotProps={{
112+
inputLabel: {
113+
shrink: true,
114+
},
115+
}}
116+
/>
117+
118+
<PasswordField
119+
label="Deploy key"
120+
placeholder={
121+
bb?.hasDeployKey
122+
? "Enter deploy key"
123+
: "Enter deploy key (optional)"
124+
}
125+
value={deployKey}
126+
required={bb?.hasDeployKey ? true : false}
127+
onChange={e => setDeployKey(e.target.value)}
128+
fullWidth
129+
variant="filled"
130+
slotProps={{
131+
inputLabel: {
132+
shrink: true,
133+
},
134+
}}
135+
/>
136+
</Box>
137+
138+
<CardFooter>
139+
<Button onClick={closeModal} disabled={loading}>
140+
Cancel
141+
</Button>
142+
<Button
143+
variant="contained"
144+
color="secondary"
145+
type="submit"
146+
disabled={loading}
147+
sx={{ ml: 2 }}
148+
>
149+
Configure
150+
</Button>
151+
</CardFooter>
152+
</Card>
153+
</Modal>
154+
);
155+
}

0 commit comments

Comments
 (0)