mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
chore(site): move templates fetching to react-query (#9622)
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import * as API from "api/api";
|
||||
|
||||
const getTemplatesQueryKey = (orgId: string) => [orgId, "templates"];
|
||||
|
||||
export const templates = (orgId: string) => {
|
||||
return {
|
||||
queryKey: getTemplatesQueryKey(orgId),
|
||||
queryFn: () => API.getTemplates(orgId),
|
||||
};
|
||||
};
|
||||
|
||||
export const templateExamples = (orgId: string) => {
|
||||
return {
|
||||
queryKey: [...getTemplatesQueryKey(orgId), "examples"],
|
||||
queryFn: () => API.getTemplateExamples(orgId),
|
||||
};
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import { TemplateExampleCard } from "components/TemplateExampleCard/TemplateExam
|
||||
import { FC } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { docs } from "utils/docs";
|
||||
import { Permissions } from "xServices/auth/authXService";
|
||||
|
||||
// Those are from https://github.com/coder/coder/tree/main/examples/templates
|
||||
const featuredExampleIds = [
|
||||
@@ -37,13 +36,13 @@ const findFeaturedExamples = (examples: TemplateExample[]) => {
|
||||
};
|
||||
|
||||
export const EmptyTemplates: FC<{
|
||||
permissions: Permissions;
|
||||
canCreateTemplates: boolean;
|
||||
examples: TemplateExample[];
|
||||
}> = ({ permissions, examples }) => {
|
||||
}> = ({ canCreateTemplates, examples }) => {
|
||||
const styles = useStyles();
|
||||
const featuredExamples = findFeaturedExamples(examples);
|
||||
|
||||
if (permissions.createTemplates) {
|
||||
if (canCreateTemplates) {
|
||||
return (
|
||||
<TableEmpty
|
||||
message="Create a Template"
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import { useMachine } from "@xstate/react";
|
||||
import { useOrganizationId } from "hooks/useOrganizationId";
|
||||
import { usePermissions } from "hooks/usePermissions";
|
||||
import { FC } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { pageTitle } from "../../utils/page";
|
||||
import { templatesMachine } from "../../xServices/templates/templatesXService";
|
||||
import { TemplatesPageView } from "./TemplatesPageView";
|
||||
import { templateExamples, templates } from "api/queries/templates";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export const TemplatesPage: FC = () => {
|
||||
const organizationId = useOrganizationId();
|
||||
const permissions = usePermissions();
|
||||
const [templatesState] = useMachine(templatesMachine, {
|
||||
context: {
|
||||
organizationId,
|
||||
permissions,
|
||||
},
|
||||
const templatesQuery = useQuery(templates(organizationId));
|
||||
const examplesQuery = useQuery({
|
||||
...templateExamples(organizationId),
|
||||
enabled: permissions.createTemplates,
|
||||
});
|
||||
const error = templatesQuery.error || examplesQuery.error;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("Templates")}</title>
|
||||
</Helmet>
|
||||
<TemplatesPageView context={templatesState.context} />
|
||||
<TemplatesPageView
|
||||
error={error}
|
||||
canCreateTemplates={permissions.createTemplates}
|
||||
examples={examplesQuery.data}
|
||||
templates={templatesQuery.data}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
mockApiError,
|
||||
MockOrganization,
|
||||
MockPermissions,
|
||||
MockTemplate,
|
||||
MockTemplateExample,
|
||||
MockTemplateExample2,
|
||||
@@ -19,32 +17,29 @@ type Story = StoryObj<typeof TemplatesPageView>;
|
||||
|
||||
export const WithTemplates: Story = {
|
||||
args: {
|
||||
context: {
|
||||
organizationId: MockOrganization.id,
|
||||
permissions: MockPermissions,
|
||||
error: undefined,
|
||||
templates: [
|
||||
MockTemplate,
|
||||
{
|
||||
...MockTemplate,
|
||||
active_user_count: -1,
|
||||
description: "🚀 Some new template that has no activity data",
|
||||
icon: "/icon/goland.svg",
|
||||
},
|
||||
{
|
||||
...MockTemplate,
|
||||
active_user_count: 150,
|
||||
description: "😮 Wow, this one has a bunch of usage!",
|
||||
icon: "",
|
||||
},
|
||||
{
|
||||
...MockTemplate,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ",
|
||||
},
|
||||
],
|
||||
examples: [],
|
||||
},
|
||||
canCreateTemplates: true,
|
||||
error: undefined,
|
||||
templates: [
|
||||
MockTemplate,
|
||||
{
|
||||
...MockTemplate,
|
||||
active_user_count: -1,
|
||||
description: "🚀 Some new template that has no activity data",
|
||||
icon: "/icon/goland.svg",
|
||||
},
|
||||
{
|
||||
...MockTemplate,
|
||||
active_user_count: 150,
|
||||
description: "😮 Wow, this one has a bunch of usage!",
|
||||
icon: "",
|
||||
},
|
||||
{
|
||||
...MockTemplate,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ",
|
||||
},
|
||||
],
|
||||
examples: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,44 +54,29 @@ export const WithTemplatesSmallViewPort: Story = {
|
||||
|
||||
export const EmptyCanCreate: Story = {
|
||||
args: {
|
||||
context: {
|
||||
organizationId: MockOrganization.id,
|
||||
permissions: MockPermissions,
|
||||
error: undefined,
|
||||
templates: [],
|
||||
examples: [MockTemplateExample, MockTemplateExample2],
|
||||
},
|
||||
canCreateTemplates: true,
|
||||
error: undefined,
|
||||
templates: [],
|
||||
examples: [MockTemplateExample, MockTemplateExample2],
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyCannotCreate: Story = {
|
||||
args: {
|
||||
context: {
|
||||
organizationId: MockOrganization.id,
|
||||
permissions: {
|
||||
...MockPermissions,
|
||||
createTemplates: false,
|
||||
},
|
||||
error: undefined,
|
||||
templates: [],
|
||||
examples: [MockTemplateExample, MockTemplateExample2],
|
||||
},
|
||||
error: undefined,
|
||||
templates: [],
|
||||
examples: [MockTemplateExample, MockTemplateExample2],
|
||||
canCreateTemplates: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
context: {
|
||||
organizationId: MockOrganization.id,
|
||||
permissions: {
|
||||
...MockPermissions,
|
||||
createTemplates: false,
|
||||
},
|
||||
error: mockApiError({
|
||||
message: "Something went wrong fetching templates.",
|
||||
}),
|
||||
templates: undefined,
|
||||
examples: undefined,
|
||||
},
|
||||
error: mockApiError({
|
||||
message: "Something went wrong fetching templates.",
|
||||
}),
|
||||
templates: undefined,
|
||||
examples: undefined,
|
||||
canCreateTemplates: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,18 +16,18 @@ import {
|
||||
formatTemplateBuildTime,
|
||||
formatTemplateActiveDevelopers,
|
||||
} from "utils/templates";
|
||||
import { AvatarData } from "../../components/AvatarData/AvatarData";
|
||||
import { Margins } from "../../components/Margins/Margins";
|
||||
import { AvatarData } from "components/AvatarData/AvatarData";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderSubtitle,
|
||||
PageHeaderTitle,
|
||||
} from "../../components/PageHeader/PageHeader";
|
||||
import { Stack } from "../../components/Stack/Stack";
|
||||
} from "components/PageHeader/PageHeader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
TableLoaderSkeleton,
|
||||
TableRowSkeleton,
|
||||
} from "../../components/TableLoader/TableLoader";
|
||||
} from "components/TableLoader/TableLoader";
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipLink,
|
||||
@@ -36,9 +36,8 @@ import {
|
||||
HelpTooltipTitle,
|
||||
} from "components/HelpTooltip/HelpTooltip";
|
||||
import { EmptyTemplates } from "./EmptyTemplates";
|
||||
import { TemplatesContext } from "xServices/templates/templatesXService";
|
||||
import { useClickableTableRow } from "hooks/useClickableTableRow";
|
||||
import { Template } from "api/typesGenerated";
|
||||
import { Template, TemplateExample } from "api/typesGenerated";
|
||||
import { combineClasses } from "utils/combineClasses";
|
||||
import { colors } from "theme/colors";
|
||||
import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined";
|
||||
@@ -141,13 +140,18 @@ const TemplateRow: FC<{ template: Template }> = ({ template }) => {
|
||||
};
|
||||
|
||||
export interface TemplatesPageViewProps {
|
||||
context: TemplatesContext;
|
||||
error?: unknown;
|
||||
examples: TemplateExample[] | undefined;
|
||||
templates: Template[] | undefined;
|
||||
canCreateTemplates: boolean;
|
||||
}
|
||||
|
||||
export const TemplatesPageView: FC<
|
||||
React.PropsWithChildren<TemplatesPageViewProps>
|
||||
> = ({ context }) => {
|
||||
const { templates, error, examples, permissions } = context;
|
||||
export const TemplatesPageView: FC<TemplatesPageViewProps> = ({
|
||||
templates,
|
||||
error,
|
||||
examples,
|
||||
canCreateTemplates,
|
||||
}) => {
|
||||
const isLoading = !templates;
|
||||
const isEmpty = Boolean(templates && templates.length === 0);
|
||||
|
||||
@@ -155,7 +159,7 @@ export const TemplatesPageView: FC<
|
||||
<Margins>
|
||||
<PageHeader
|
||||
actions={
|
||||
<Maybe condition={permissions.createTemplates}>
|
||||
<Maybe condition={canCreateTemplates}>
|
||||
<Button component={RouterLink} to="/starter-templates">
|
||||
Starter Templates
|
||||
</Button>
|
||||
@@ -208,7 +212,7 @@ export const TemplatesPageView: FC<
|
||||
<ChooseOne>
|
||||
<Cond condition={isEmpty}>
|
||||
<EmptyTemplates
|
||||
permissions={permissions}
|
||||
canCreateTemplates={canCreateTemplates}
|
||||
examples={examples ?? []}
|
||||
/>
|
||||
</Cond>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Permissions } from "xServices/auth/authXService";
|
||||
import { assign, createMachine } from "xstate";
|
||||
import * as API from "../../api/api";
|
||||
import * as TypesGen from "../../api/typesGenerated";
|
||||
|
||||
export interface TemplatesContext {
|
||||
organizationId: string;
|
||||
permissions: Permissions;
|
||||
templates?: TypesGen.Template[];
|
||||
examples?: TypesGen.TemplateExample[];
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export const templatesMachine = createMachine(
|
||||
{
|
||||
id: "templatesState",
|
||||
predictableActionArguments: true,
|
||||
tsTypes: {} as import("./templatesXService.typegen").Typegen0,
|
||||
schema: {
|
||||
context: {} as TemplatesContext,
|
||||
services: {} as {
|
||||
load: {
|
||||
data: {
|
||||
templates: TypesGen.Template[];
|
||||
examples: TypesGen.TemplateExample[];
|
||||
};
|
||||
};
|
||||
},
|
||||
},
|
||||
initial: "loading",
|
||||
states: {
|
||||
loading: {
|
||||
invoke: {
|
||||
src: "load",
|
||||
id: "load",
|
||||
onDone: {
|
||||
actions: ["assignData"],
|
||||
target: "idle",
|
||||
},
|
||||
onError: {
|
||||
actions: "assignError",
|
||||
target: "idle",
|
||||
},
|
||||
},
|
||||
},
|
||||
idle: {
|
||||
type: "final",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
assignData: assign({
|
||||
templates: (_, event) => event.data.templates,
|
||||
examples: (_, event) => event.data.examples,
|
||||
}),
|
||||
assignError: assign({
|
||||
error: (_, { data }) => data,
|
||||
}),
|
||||
},
|
||||
services: {
|
||||
load: async ({ organizationId, permissions }) => {
|
||||
const [templates, examples] = await Promise.all([
|
||||
API.getTemplates(organizationId),
|
||||
permissions.createTemplates
|
||||
? API.getTemplateExamples(organizationId)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
return {
|
||||
templates,
|
||||
examples,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user