Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 16 additions & 12 deletions site/src/api/queries/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import type { QueryClient } from "react-query";
import { API } from "#/api/api";
import type * as TypesGen from "#/api/typesGenerated";

const appsKey = ["oauth2-provider", "apps"];
const userAppsKey = (userId: string) => appsKey.concat(userId);
const appKey = (appId: string) => appsKey.concat(appId);
const appSecretsKey = (appId: string) => appKey(appId).concat("secrets");
const oauth2ProviderAppsKey = ["oauth2-provider", "apps"];
export const oauth2ProviderAppKey = (appId: string) =>
oauth2ProviderAppsKey.concat(appId);
export const oauth2ProviderAppSecretsKey = (appId: string) =>
oauth2ProviderAppKey(appId).concat("secrets");

const userAppsKey = (userId: string) => oauth2ProviderAppsKey.concat(userId);

export const getGitHubDevice = () => {
return {
Expand All @@ -23,14 +26,14 @@ export const getGitHubDeviceFlowCallback = (code: string, state: string) => {

export const getApps = (userId?: string) => {
return {
queryKey: userId ? appsKey.concat(userId) : appsKey,
queryKey: userId ? userAppsKey(userId) : oauth2ProviderAppsKey,
queryFn: () => API.getOAuth2ProviderApps({ user_id: userId }),
};
};

export const getApp = (id: string) => {
return {
queryKey: appKey(id),
queryKey: oauth2ProviderAppKey(id),
queryFn: () => API.getOAuth2ProviderApp(id),
};
};
Expand All @@ -40,7 +43,7 @@ export const postApp = (queryClient: QueryClient) => {
mutationFn: API.postOAuth2ProviderApp,
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: appsKey,
queryKey: oauth2ProviderAppsKey,
});
},
};
Expand All @@ -56,8 +59,9 @@ export const putApp = (queryClient: QueryClient) => {
req: TypesGen.PutOAuth2ProviderAppRequest;
}) => API.putOAuth2ProviderApp(id, req),
onSuccess: async (app: TypesGen.OAuth2ProviderApp) => {
queryClient.setQueryData(oauth2ProviderAppKey(app.id), app);
await queryClient.invalidateQueries({
queryKey: appKey(app.id),
queryKey: oauth2ProviderAppsKey,
});
},
};
Expand All @@ -68,15 +72,15 @@ export const deleteApp = (queryClient: QueryClient) => {
mutationFn: API.deleteOAuth2ProviderApp,
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: appsKey,
queryKey: oauth2ProviderAppsKey,
});
},
};
};

export const getAppSecrets = (id: string) => {
return {
queryKey: appSecretsKey(id),
queryKey: oauth2ProviderAppSecretsKey(id),
queryFn: () => API.getOAuth2ProviderAppSecrets(id),
};
};
Expand All @@ -89,7 +93,7 @@ export const postAppSecret = (queryClient: QueryClient) => {
appId: string,
) => {
await queryClient.invalidateQueries({
queryKey: appSecretsKey(appId),
queryKey: oauth2ProviderAppSecretsKey(appId),
});
},
};
Expand All @@ -101,7 +105,7 @@ export const deleteAppSecret = (queryClient: QueryClient) => {
API.deleteOAuth2ProviderAppSecret(appId, secretId),
onSuccess: async (_: unknown, { appId }: { appId: string }) => {
await queryClient.invalidateQueries({
queryKey: appSecretsKey(appId),
queryKey: oauth2ProviderAppSecretsKey(appId),
});
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,3 @@
import type { FC } from "react";
import { useMutation, useQueryClient } from "react-query";
import { useNavigate, useSearchParams } from "react-router";
import { toast } from "sonner";
import { getErrorDetail } from "#/api/errors";
import { postApp } from "#/api/queries/oauth2";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { pageTitle } from "#/utils/page";
import { CreateOAuth2AppPageView } from "./CreateOAuth2AppPageView";

const CreateOAuth2AppPage: FC = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { permissions } = useAuthenticated();
const queryClient = useQueryClient();
const postAppMutation = useMutation(postApp(queryClient));
const canCreateApp = permissions.createOAuth2App;

const defaultValues = {
name: searchParams.get("name") ?? "",
callback_url: searchParams.get("callback_url") ?? "",
icon: searchParams.get("icon") ?? "",
};

return (
<>
<title>{pageTitle("New OAuth2 Application")}</title>

<CreateOAuth2AppPageView
isUpdating={postAppMutation.isPending}
error={postAppMutation.error}
defaultValues={defaultValues}
createApp={async (req) => {
const mutation = postAppMutation.mutateAsync(req, {
onSuccess: (app) => {
navigate(
`/deployment/oauth2-provider/apps/${app.id}?created=true`,
);
},
});
toast.promise(mutation, {
loading: `Creating OAuth2 application "${req.name}"...`,
success: (app) =>
`OAuth2 application "${app.name}" created successfully.`,
error: (error) => ({
message: `Failed to create "${req.name}" OAuth2 application.`,
description: getErrorDetail(error),
}),
});
}}
canCreateApp={canCreateApp}
/>
</>
);
};

export default CreateOAuth2AppPage;
export default CreateOAuth2AppPageView;
Original file line number Diff line number Diff line change
@@ -1,50 +1,109 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { mockApiError } from "#/testHelpers/entities";
import { expect, spyOn, userEvent, within } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import { API } from "#/api/api";
import {
MockPermissions,
MockUserOwner,
mockApiError,
} from "#/testHelpers/entities";
import { withAuthProvider, withToaster } from "#/testHelpers/storybook";
import { CreateOAuth2AppPageView } from "./CreateOAuth2AppPageView";

const meta: Meta = {
const meta = {
title: "pages/DeploymentSettingsPage/CreateOAuth2AppPageView",
component: CreateOAuth2AppPageView,
args: {
canCreateApp: true,
parameters: {
user: MockUserOwner,
permissions: MockPermissions,
reactRouter: reactRouterParameters({
location: { path: "/deployment/oauth2-provider/apps/add" },
routing: [
{ path: "/deployment/oauth2-provider/apps", useStoryElement: true },
{
path: "/deployment/oauth2-provider/apps/add",
useStoryElement: true,
},
],
}),
},
};
export default meta;
decorators: [withToaster, withAuthProvider],
} satisfies Meta<typeof CreateOAuth2AppPageView>;

export default meta;
type Story = StoryObj<typeof CreateOAuth2AppPageView>;

export const Updating: Story = {
args: {
isUpdating: true,
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
await canvas.findByRole("heading", {
name: /add an oauth2 application/i,
}),
).toBeVisible();
await expect(
canvas.getByRole("button", { name: /create application/i }),
).toBeDisabled();
},
};

export const WithError: Story = {
args: {
error: mockApiError({
message: "Validation failed",
validations: [
{
field: "name",
detail: "name error",
},
{
field: "callback_url",
detail: "url error",
},
{
field: "icon",
detail: "icon error",
},
],
}),
export const WithValidationError: Story = {
beforeEach: () => {
spyOn(API, "postOAuth2ProviderApp").mockRejectedValue(
mockApiError({
message: "Validation failed",
validations: [
{ field: "name", detail: "name error" },
{ field: "callback_url", detail: "url error" },
{ field: "icon", detail: "icon error" },
],
}),
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(await canvas.findByLabelText(/^name/i), "test-app");
await userEvent.type(
canvas.getByLabelText(/callback url/i),
"https://example.com/callback",
);
await userEvent.click(
canvas.getByRole("button", { name: /create application/i }),
);
await expect(await canvas.findByText("name error")).toBeVisible();
await expect(canvas.getByText("url error")).toBeVisible();
await expect(canvas.getByText("icon error")).toBeVisible();
},
};

export const NoPermissions: Story = {
args: {
canCreateApp: false,
export const InvalidName: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nameInput = await canvas.findByLabelText(/^name/i);
await userEvent.type(nameInput, "Foo@Application");
await userEvent.tab();
await expect(
await canvas.findByText(
/special characters \(e\.g\.: !, @, #\) are not supported/i,
),
).toBeVisible();
await expect(
canvas.getByRole("button", { name: /create application/i }),
).toBeDisabled();
},
};

export const Default: Story = {};
export const NoPermissions: Story = {
parameters: {
permissions: {
...MockPermissions,
createOAuth2App: false,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
await canvas.findByRole("button", { name: /create application/i }),
).toBeDisabled();
},
};
Loading
Loading