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

Commit 17e6003

Browse files
committed
feat: allow creating bare apps and upload zip deployments
1 parent be0c7dd commit 17e6003

25 files changed

Lines changed: 812 additions & 359 deletions

File tree

src/components/AppName/AppName.spec.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe("~/components/AppName/AppName.tsx", () => {
3232
it("should display the app name with the display name and repo", () => {
3333
createWrapper({ app });
3434

35+
expect(() => wrapper.getByTestId("BoltIcon")).toThrow();
3536
expect(wrapper.getByText("app")).toBeTruthy();
3637
expect(wrapper.getByText("app").getAttribute("href")).toBe(
3738
`/apps/${app.id}/environments`
@@ -42,4 +43,16 @@ describe("~/components/AppName/AppName.tsx", () => {
4243
"https://gitlab.com/stormkit-io/frontend"
4344
);
4445
});
46+
47+
it("should display only the name if an app is bare", () => {
48+
createWrapper({ app: { ...app, isBare: true } });
49+
50+
expect(wrapper.container.textContent).toBe("app");
51+
expect(wrapper.getByTestId("BoltIcon")).toBeTruthy();
52+
expect(wrapper.getByText("app").getAttribute("href")).toBe(
53+
`/apps/${app.id}/environments`
54+
);
55+
56+
expect(() => wrapper.getByText("stormkit-io/frontend")).toThrow();
57+
});
4558
});

src/components/AppName/AppName.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import Link from "@mui/material/Link";
22
import Box from "@mui/material/Box";
33
import Typography from "@mui/material/Typography";
4+
import BoltIcon from "@mui/icons-material/Bolt";
45
import Dot from "~/components/Dot";
6+
import IconBg from "~/components/IconBg";
57
import { getLogoForProvider, parseRepo } from "~/utils/helpers/providers";
68

79
interface Props {
@@ -15,7 +17,25 @@ const providerHosts: Record<Provider, string> = {
1517
gitlab: "gitlab.com",
1618
};
1719

18-
export default function AppName({ app, imageSize = 18 }: Props) {
20+
export default function AppName({ app, imageSize = 20 }: Props) {
21+
if (app.isBare) {
22+
return (
23+
<Typography sx={{ display: "flex", alignItems: "center" }}>
24+
<IconBg
25+
sx={{
26+
width: imageSize,
27+
height: imageSize,
28+
mr: 1,
29+
bgcolor: "text.primary",
30+
}}
31+
>
32+
<BoltIcon sx={{ ml: 0, fontSize: 16, color: "container.paper" }} />
33+
</IconBg>
34+
<Link href={`/apps/${app.id}/environments`}>{app.displayName}</Link>
35+
</Typography>
36+
);
37+
}
38+
1939
const { repo, provider } = parseRepo(app.repo);
2040
const providerLogo = getLogoForProvider(provider);
2141
const linkToRepo = `https://${providerHosts[provider]}/${repo}`;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, it, vi, beforeEach, type Mock } from "vitest";
2+
import { type RenderResult, render } from "@testing-library/react";
3+
import MyDropzone from "./Dropzone";
4+
5+
interface Props {
6+
openDialog?: { mode: string };
7+
loading?: boolean;
8+
files?: File[];
9+
clickToOpen?: boolean;
10+
showDropZone?: boolean;
11+
}
12+
13+
describe("~/components/Dropzone.tsx", () => {
14+
let wrapper: RenderResult;
15+
let onDrop: Mock;
16+
17+
const createWrapper = ({
18+
showDropZone,
19+
clickToOpen,
20+
openDialog,
21+
files = [],
22+
}: Props) => {
23+
onDrop = vi.fn();
24+
wrapper = render(
25+
<MyDropzone
26+
openDialog={openDialog}
27+
onDrop={onDrop}
28+
files={files}
29+
showDropZone={showDropZone}
30+
clickToOpen={clickToOpen}
31+
/>
32+
);
33+
};
34+
35+
describe("with no files and showDropZone = true ", () => {
36+
beforeEach(() => {
37+
createWrapper({ showDropZone: true, clickToOpen: true });
38+
});
39+
40+
it("should mount dropzone", () => {
41+
expect(wrapper.getByTestId("my-dropzone")).toBeTruthy();
42+
});
43+
44+
it("should display an empty message", () => {
45+
expect(wrapper.getByText("No files uploaded yet")).toBeTruthy();
46+
});
47+
48+
it("should display a click here message", () => {
49+
expect(wrapper.getByText("Click or drop here")).toBeTruthy();
50+
});
51+
});
52+
53+
describe("with no files and showDropZone = false ", () => {
54+
beforeEach(() => {
55+
createWrapper({ showDropZone: false });
56+
});
57+
58+
it("should mount dropzone", () => {
59+
expect(wrapper.getByTestId("my-dropzone")).toBeTruthy();
60+
});
61+
62+
it("should not display an empty message", () => {
63+
expect(() => wrapper.getByText("No files uploaded yet")).toThrow();
64+
});
65+
66+
it("should not display a drop here message", () => {
67+
expect(() => wrapper.getByText("Drop here")).toThrow();
68+
});
69+
});
70+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { useEffect, useState } from "react";
2+
import { useDropzone } from "react-dropzone";
3+
import Box from "@mui/material/Box";
4+
import { Typography } from "@mui/material";
5+
6+
interface Props {
7+
props?: { accept?: string };
8+
files?: File[];
9+
onDrop: (acceptedFiles: File[]) => void;
10+
showDropZone?: boolean; // Whether to show the dropzone area or not
11+
clickToOpen?: boolean;
12+
openDialog?: { mode: string };
13+
}
14+
15+
export default function MyDropzone({
16+
openDialog,
17+
showDropZone,
18+
clickToOpen,
19+
files,
20+
onDrop,
21+
props = {},
22+
}: Props) {
23+
const [otherProps, setOtherProps] = useState<Record<string, any>>(props);
24+
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
25+
onDrop,
26+
noClick: true,
27+
});
28+
29+
useEffect(() => {
30+
setOtherProps(
31+
openDialog?.mode === "folder"
32+
? {
33+
...otherProps,
34+
webkitdirectory: "",
35+
directory: "",
36+
mozdirectory: "",
37+
}
38+
: { ...otherProps }
39+
);
40+
}, [openDialog]);
41+
42+
useEffect(() => {
43+
openDialog && open();
44+
}, [otherProps]);
45+
46+
return (
47+
<Box {...getRootProps()} sx={{ position: "relative", py: 12 }}>
48+
{(isDragActive || showDropZone) && (
49+
<Box
50+
onClick={clickToOpen ? open : undefined}
51+
sx={{
52+
cursor: clickToOpen ? "pointer" : "default",
53+
position: "absolute",
54+
left: 0,
55+
top: 0,
56+
right: 0,
57+
bottom: 0,
58+
border: "3px dashed",
59+
borderColor: isDragActive
60+
? "container.borderContrast"
61+
: "container.border",
62+
bgcolor: "container.transparent",
63+
display: "flex",
64+
alignItems: "center",
65+
justifyContent: "center",
66+
color: clickToOpen ? "text.secondary" : "text.primary",
67+
":hover": {
68+
color: clickToOpen ? "text.primary" : undefined,
69+
},
70+
}}
71+
>
72+
<Typography
73+
sx={{
74+
fontSize: 32,
75+
}}
76+
>
77+
{clickToOpen ? "Click or drop here" : "Drop here"}
78+
</Typography>
79+
<Typography
80+
sx={{
81+
color: "text.secondary",
82+
position: "absolute",
83+
bottom: 0,
84+
mb: 2,
85+
}}
86+
>
87+
{files?.length
88+
? `Files: ${files.length}/${files.length}`
89+
: "No files uploaded yet"}
90+
</Typography>
91+
</Box>
92+
)}
93+
<input
94+
{...getInputProps()}
95+
{...otherProps}
96+
type="file"
97+
data-testid="my-dropzone"
98+
/>
99+
</Box>
100+
);
101+
}

src/components/Dropzone/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "./Dropzone";

src/components/IconBg/IconBg.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import Box from "@mui/material/Box";
1+
import Box, { BoxProps } from "@mui/material/Box";
22

3-
interface Props {
3+
interface Props extends BoxProps {
44
children: React.ReactNode;
55
}
66

7-
export default function IconBg({ children }: Props) {
7+
export default function IconBg({ children, sx }: Props) {
88
return (
99
<Box
1010
component="span"
@@ -17,6 +17,7 @@ export default function IconBg({ children }: Props) {
1717
display: "inline-flex",
1818
alignItems: "center",
1919
justifyContent: "center",
20+
...sx,
2021
}}
2122
>
2223
{children}

0 commit comments

Comments
 (0)