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

Commit 13285f9

Browse files
committed
chore: link users to admin page
1 parent c0d81c2 commit 13285f9

File tree

12 files changed

+174
-77
lines changed

12 files changed

+174
-77
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { SxProps } from "@mui/material/styles";
2+
import Box from "@mui/material/Box";
3+
import Typography from "@mui/material/Typography";
4+
import Tooltip from "@mui/material/Tooltip";
5+
import AnnouncementIcon from "@mui/icons-material/Announcement";
6+
7+
interface Props {
8+
placement?: "top" | "bottom" | "left" | "right";
9+
title?: string;
10+
subtitle?: string;
11+
sx?: SxProps;
12+
}
13+
14+
export default function ActionRequired({
15+
title,
16+
subtitle,
17+
sx,
18+
placement = "top",
19+
}: Props) {
20+
return (
21+
<Tooltip
22+
title={
23+
<Box sx={{ p: 1 }}>
24+
<Typography variant="subtitle1" sx={{ mb: subtitle ? 2 : 0 }}>
25+
{title || "Action required"}
26+
</Typography>
27+
{subtitle && <Typography variant="body2">{subtitle}</Typography>}
28+
</Box>
29+
}
30+
placement={placement}
31+
arrow
32+
>
33+
<AnnouncementIcon color="warning" sx={sx} />
34+
</Tooltip>
35+
);
36+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "./ActionRequired";

src/components/ButtonDropdown/ButtonDropdown.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState } from "react";
2+
import Box from "@mui/material/Box";
23
import Button from "@mui/material/Button";
34
import ArrowDropDown from "@mui/icons-material/ArrowDropDown";
45
import ArrowDropUp from "@mui/icons-material/ArrowDropUp";
@@ -12,6 +13,7 @@ import ClickAwayListener from "@mui/material/ClickAwayListener";
1213
interface MenuItemProps {
1314
text: string;
1415
icon?: React.ReactNode;
16+
endIcon?: React.ReactNode;
1517
href?: string;
1618
onClick?: React.MouseEventHandler;
1719
}
@@ -57,7 +59,10 @@ export default function ButtonDropdown({ buttonText, children, items }: Props) {
5759
}}
5860
>
5961
{item.icon && <ListItemIcon>{item.icon}</ListItemIcon>}
60-
{item.text}
62+
<Box component="span" sx={{ flexGrow: 1 }}>
63+
{item.text}
64+
</Box>
65+
{item.endIcon}
6166
</Link>
6267
</MenuItem>
6368
);

src/pages/apps/Apps.spec.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,18 @@ describe("~/pages/apps/Apps.tsx", () => {
3535
{
3636
path: "*",
3737
element: (
38-
<AuthContext.Provider value={{ user: mockUser(), teams }}>
38+
<AuthContext.Provider
39+
value={{
40+
user: mockUser(),
41+
teams,
42+
providers: {
43+
github: false,
44+
gitlab: false,
45+
bitbucket: false,
46+
[provider as Provider]: true,
47+
},
48+
}}
49+
>
3950
<Apps />
4051
</AuthContext.Provider>
4152
),
@@ -87,9 +98,11 @@ describe("~/pages/apps/Apps.tsx", () => {
8798
fireEvent.click(button!);
8899

89100
await waitFor(() => {
90-
expect(wrapper.getByText("Import from URL").getAttribute("href")).toBe(
91-
"/apps/new/url"
92-
);
101+
expect(
102+
wrapper
103+
.getByRole("link", { name: "Import from URL" })
104+
.getAttribute("href")
105+
).toBe("/apps/new/url");
93106
});
94107
});
95108

@@ -186,7 +199,7 @@ describe("~/pages/apps/Apps.tsx", () => {
186199
await waitFor(() => {
187200
expect(
188201
wrapper.getByText(
189-
"Configure authentication to import from private repositories"
202+
"Configure provider to import from private repositories"
190203
)
191204
).toBeTruthy();
192205
});

src/pages/apps/Apps.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ import { providerToText } from "~/utils/helpers/string";
2323
import { useFetchAppList, createApp } from "./actions";
2424
import { WelcomeModal, EmptyList } from "./_components";
2525
import TeamStats from "./_components/TeamStats";
26+
import ActionRequired from "~/components/ActionRequired";
2627

2728
let timeout: NodeJS.Timeout;
2829
const limit = 20;
2930
const welcomeModalId = "welcome_modal";
3031

3132
export default function Apps() {
32-
const { teams } = useContext(AuthContext);
33+
const { teams, providers } = useContext(AuthContext);
3334
const [from, setFrom] = useState(0);
3435
const [filter, setFilter] = useState("");
3536
const selectedTeam = useSelectedTeam({ teams });
@@ -45,7 +46,11 @@ export default function Apps() {
4546
LocalStorage.get(welcomeModalId) !== "shown"
4647
);
4748

48-
const provider = LocalStorage.get<Provider>(LS_PROVIDER);
49+
let provider = LocalStorage.get<Provider>(LS_PROVIDER);
50+
51+
if (!providers?.[provider as Provider]) {
52+
provider = undefined;
53+
}
4954

5055
if (loading && !filter) {
5156
return (
@@ -67,10 +72,8 @@ export default function Apps() {
6772
<Box sx={{ width: "100%" }} maxWidth="lg">
6873
<Card sx={{ px: { xs: 1, md: 4 } }}>
6974
<EmptyList
70-
primaryActionText="Configure authentication"
71-
primaryDesc="Configure authentication to import from private repositories
72-
73-
"
75+
primaryActionText="Configure provider"
76+
primaryDesc="Configure provider to import from private repositories"
7477
primaryLink="https://www.stormkit.io/docs/self-hosting/authentication"
7578
secondaryLink="/apps/new/url"
7679
secondaryActionText="Import from URL"
@@ -84,11 +87,9 @@ export default function Apps() {
8487

8588
const importFromProvider = provider
8689
? `Import from ${providerToText[provider]}`
87-
: "Configure authentication";
90+
: "Configure provider";
8891

89-
const newAppHref = provider
90-
? `/apps/new/${provider}`
91-
: "https://www.stormkit.io/docs/self-hosting/authentication";
92+
const newAppHref = provider ? `/apps/new/${provider}` : "/admin/git";
9293

9394
if (apps.length === 0 && !filter) {
9495
return (
@@ -131,6 +132,13 @@ export default function Apps() {
131132
items={[
132133
{
133134
icon: <ImportExport />,
135+
endIcon: provider ? undefined : (
136+
<ActionRequired
137+
placement="bottom"
138+
subtitle="Configure provider to import from private repositories"
139+
title="Action required"
140+
/>
141+
),
134142
text: importFromProvider,
135143
href: newAppHref,
136144
},
@@ -164,8 +172,8 @@ export default function Apps() {
164172
setFilter(e.target.value);
165173
}, 250);
166174
}}
167-
InputProps={{
168-
endAdornment: <SearchIcon sx={{ fontSize: 16 }} />,
175+
slotProps={{
176+
input: { endAdornment: <SearchIcon sx={{ fontSize: 16 }} /> },
169177
}}
170178
/>
171179
</Box>

src/pages/auth/Auth.context.spec.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RenderResult } from "@testing-library/react";
22
import type { AuthContextProps } from "~/pages/auth/Auth.context";
33
import type { Scope } from "nock";
4+
import nock from "nock";
45
import { describe, expect, beforeEach, afterEach, it, vi } from "vitest";
56
import { waitFor } from "@testing-library/react";
67
import { LocalStorage } from "~/utils/storage";
@@ -21,7 +22,28 @@ describe("pages/auth/Auth.context", () => {
2122
let scope: Scope;
2223
let context: AuthContextProps;
2324

25+
const mockFetchAuthProviders = ({
26+
status,
27+
response,
28+
}: {
29+
status: number;
30+
response: {
31+
github: boolean;
32+
gitlab: boolean;
33+
bitbucket: boolean;
34+
basicAuth?: "enabled";
35+
};
36+
}) =>
37+
nock(process.env.API_DOMAIN || "")
38+
.get(`/auth/providers`)
39+
.reply(status, response);
40+
2441
const createWrapper = () => {
42+
mockFetchAuthProviders({
43+
status: 200,
44+
response: { github: true, gitlab: true, bitbucket: true },
45+
});
46+
2547
wrapper = renderWithRouter({
2648
initialEntries: ["/apps/1231231?my-query=1"],
2749
el: () => (
@@ -111,11 +133,16 @@ describe("pages/auth/Auth.context", () => {
111133
displayName: "stormkit",
112134
},
113135
],
114-
authError: null,
136+
authError: undefined,
115137
loginOauth: expect.any(Function),
116138
logout: expect.any(Function),
117139
reloadTeams: expect.any(Function),
118140
teams: undefined,
141+
providers: {
142+
github: true,
143+
gitlab: true,
144+
bitbucket: true,
145+
},
119146
});
120147
});
121148
});

src/pages/auth/Auth.context.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import type { LoginOauthReturnValue } from "./actions";
2+
import CircularProgress from "@mui/material/CircularProgress";
23
import React, { createContext, useEffect, useState } from "react";
34
import { useNavigate, useLocation } from "react-router-dom";
4-
import Spinner from "~/components/Spinner";
55
import * as actions from "./actions";
6+
import type { Providers } from "./actions";
67
import UpdateSnackbar from "./UpdateSnackbar";
78

8-
const { loginOauth, useFetchUser, useFetchTeams, logout } = actions;
9+
const {
10+
loginOauth,
11+
useFetchUser,
12+
useFetchTeams,
13+
logout,
14+
useFetchActiveProviders,
15+
} = actions;
916

1017
export interface AuthContextProps {
1118
user?: User;
1219
authError?: string | null;
1320
accounts?: Array<ConnectedAccount>;
21+
providers?: Providers;
1422
logout?: () => void;
1523
loginOauth?: (p: Provider) => Promise<LoginOauthReturnValue>;
1624
reloadTeams?: () => void;
@@ -28,6 +36,11 @@ export default function ContextProvider({ children }: Props) {
2836
const [refreshToken, setTeamsRefreshToken] = useState(0);
2937
const { pathname, search } = useLocation();
3038
const { error, loading, user, accounts, ...fns } = useFetchUser();
39+
const {
40+
providers,
41+
loading: pLoading,
42+
error: pError,
43+
} = useFetchActiveProviders();
3144
const { teams, loading: tLoading } = useFetchTeams({ refreshToken, user });
3245

3346
const shouldRedirect = !loading && !user && !pathname.includes("/auth");
@@ -49,8 +62,18 @@ export default function ContextProvider({ children }: Props) {
4962
}
5063
}, [user?.isPaymentRequired, pathname]);
5164

52-
if (loading || tLoading) {
53-
return <Spinner primary pageCenter />;
65+
if (loading || tLoading || pLoading) {
66+
return (
67+
<CircularProgress
68+
sx={{
69+
position: "absolute",
70+
top: "50%",
71+
left: "50%",
72+
transform: "translate(-50%, -50%)",
73+
m: 0,
74+
}}
75+
/>
76+
);
5477
}
5578

5679
// Redirect the user to the console login if he/she
@@ -64,8 +87,9 @@ export default function ContextProvider({ children }: Props) {
6487
value={{
6588
user,
6689
accounts,
67-
authError: error,
90+
authError: error || pError,
6891
teams,
92+
providers,
6993
reloadTeams: () => setTeamsRefreshToken(Date.now()),
7094
logout: logout(), // This function can be removed, it's no longer being injected something.
7195
loginOauth: loginOauth({ ...fns }),

0 commit comments

Comments
 (0)