From 0a5c4310182d58ad9d1f99516f318a5930502e4b Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 1 Jul 2024 22:00:15 +0000 Subject: [PATCH 01/25] feat: initial changes for multi-org templates page --- .../src/pages/TemplatesPage/TemplatesPage.tsx | 31 ++- .../TemplatesPage/TemplatesPageViewV2.tsx | 186 ++++++++++++++++++ 2 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index 75c98d5221320..948ba5cfa890c 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -2,33 +2,48 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { templateExamples, templates } from "api/queries/templates"; +import { myOrganizations } from "api/queries/users"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useDashboard } from "modules/dashboard/useDashboard"; import { pageTitle } from "utils/page"; import { TemplatesPageView } from "./TemplatesPageView"; +import { TemplatesPageViewV2 } from "./TemplatesPageViewV2"; export const TemplatesPage: FC = () => { const { permissions } = useAuthenticated(); - const { organizationId } = useDashboard(); + const { organizationId, experiments } = useDashboard(); + const organizationsQuery = useQuery(myOrganizations()); const templatesQuery = useQuery(templates(organizationId)); const examplesQuery = useQuery({ ...templateExamples(organizationId), enabled: permissions.createTemplates, }); - const error = templatesQuery.error || examplesQuery.error; + const error = templatesQuery.error || examplesQuery.error || organizationsQuery.error; + const multiOrgExperimentEnabled = experiments.includes("multi-organization"); + console.log({ multiOrgExperimentEnabled }) return ( <> {pageTitle("Templates")} - + {multiOrgExperimentEnabled ? ( + + ) : ( + + )} ); }; diff --git a/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx b/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx new file mode 100644 index 0000000000000..3c4a6982e838e --- /dev/null +++ b/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx @@ -0,0 +1,186 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import type { FC } from "react"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import type { Organization, Template, TemplateExample } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { + HelpTooltip, + HelpTooltipContent, + HelpTooltipLink, + HelpTooltipLinksGroup, + HelpTooltipText, + HelpTooltipTitle, + HelpTooltipTrigger, +} from "components/HelpTooltip/HelpTooltip"; +// import { Loader } from "components/Loader/Loader"; +import { Margins } from "components/Margins/Margins"; +import { + PageHeader, + PageHeaderSubtitle, + PageHeaderTitle, +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +// import { createDayString } from "utils/createDayString"; +import { docs } from "utils/docs"; +import { + formatTemplateBuildTime, + formatTemplateActiveDevelopers, +} from "utils/templates"; +import { CreateTemplateButton } from "./CreateTemplateButton"; +import { EmptyTemplates } from "./EmptyTemplates"; + +export const Language = { + developerCount: (activeCount: number): string => { + return `${formatTemplateActiveDevelopers(activeCount)} developer${ + activeCount !== 1 ? "s" : "" + }`; + }, + nameLabel: "Name", + buildTimeLabel: "Build time", + usedByLabel: "Used by", + lastUpdatedLabel: "Last updated", + templateTooltipTitle: "What is template?", + templateTooltipText: + "With templates you can create a common configuration for your workspaces using Terraform.", + templateTooltipLink: "Manage templates", +}; + +const TemplateHelpTooltip: FC = () => { + return ( + + + + {Language.templateTooltipTitle} + {Language.templateTooltipText} + + + {Language.templateTooltipLink} + + + + + ); +}; + +export interface TemplatesPageViewProps { + templates: Template[] | undefined; + organizations: Organization[] | undefined; + examples: TemplateExample[] | undefined; + canCreateTemplates: boolean; + error?: unknown; +} + +export const TemplatesPageViewV2: FC = ({ + templates, + organizations, + examples, + canCreateTemplates, + error, +}) => { + const [urlParams] = useSearchParams(); + const isEmpty = templates && templates.length === 0; + const navigate = useNavigate(); + + const activeOrg = urlParams.get("org") ?? "all"; + + return ( + + + } + > + + + Templates + + + + {templates && templates.length > 0 && ( + + Select a template to create a workspace. + + )} + + + {Boolean(error) && } + + {/* {Boolean(isLoading) && } */} + + + + ORGANIZATION + {organizations?.map((org) => ( + + {/* {org.name} ({starterTemplatesByTag[tag].length}) */} + {org.name} (1) + + ))} + + + +
+ {isEmpty ? ( + + ) : (templates && + templates.map((template) => ( +

{template.name}

+ )))} +
+
+
+ ); +}; + +const styles = { + filterCaption: (theme) => ({ + textTransform: "uppercase", + fontWeight: 600, + fontSize: 12, + color: theme.palette.text.secondary, + letterSpacing: "0.1em", + }), + tagLink: (theme) => ({ + color: theme.palette.text.secondary, + textDecoration: "none", + fontSize: 14, + textTransform: "capitalize", + + "&:hover": { + color: theme.palette.text.primary, + }, + }), + tagLinkActive: (theme) => ({ + color: theme.palette.text.primary, + fontWeight: 600, + }), + secondary: (theme) => ({ + color: theme.palette.text.secondary, + }), + actionButton: (theme) => ({ + transition: "none", + color: theme.palette.text.secondary, + "&:hover": { + borderColor: theme.palette.text.primary, + }, + }), +} satisfies Record>; From 6f96fee53273fe5745e2561a0e1628364b2bdaa4 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 2 Jul 2024 19:57:04 +0000 Subject: [PATCH 02/25] feat: add TemplateCard component --- .../templates/TemplateCard/TemplateCard.tsx | 141 ++++++++++++++++++ .../src/pages/TemplatesPage/TemplatesPage.tsx | 1 - .../TemplatesPage/TemplatesPageViewV2.tsx | 22 +-- 3 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 site/src/modules/templates/TemplateCard/TemplateCard.tsx diff --git a/site/src/modules/templates/TemplateCard/TemplateCard.tsx b/site/src/modules/templates/TemplateCard/TemplateCard.tsx new file mode 100644 index 0000000000000..b1868b34a33c2 --- /dev/null +++ b/site/src/modules/templates/TemplateCard/TemplateCard.tsx @@ -0,0 +1,141 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"; +import Button from "@mui/material/Button"; +import type { FC, HTMLAttributes } from "react"; +import { Link as RouterLink } from "react-router-dom"; +import type { Template } from "api/typesGenerated"; +import { ExternalAvatar } from "components/Avatar/Avatar"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import { DeprecatedBadge } from "components/Badges/Badges"; + +type TemplateCardProps = HTMLAttributes & { + template: Template; +}; + +export const TemplateCard: FC = ({ + template, + ...divProps +}) => { + const hasIcon = template.icon && template.icon !== ""; + + return ( +
+
+
+ 0 + ? template.display_name + : template.name + } + subtitle={template.organization_name} + avatar={ + hasIcon && ( + + ) + } + /> +
+
+ {template.active_user_count} {template.active_user_count === 1 ? "user" : "users" } +
+
+ +
+ +

{template.description}

+
+
+ +
+ {template.deprecated ? ( + + ) : ( + + )} +
+
+ ); +}; + +const styles = { + card: (theme) => ({ + width: "320px", + padding: 24, + borderRadius: 6, + border: `1px solid ${theme.palette.divider}`, + textAlign: "left", + color: "inherit", + display: "flex", + flexDirection: "column", + }), + + header: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + marginBottom: 24, + }, + + icon: { + flexShrink: 0, + paddingTop: 4, + width: 32, + height: 32, + }, + + tags: { + display: "flex", + flexWrap: "wrap", + gap: 8, + justifyContent: "end", + }, + + tag: (theme) => ({ + borderColor: theme.palette.divider, + textDecoration: "none", + cursor: "pointer", + "&: hover": { + borderColor: theme.palette.primary.main, + }, + }), + + activeTag: (theme) => ({ + borderColor: theme.roles.active.outline, + backgroundColor: theme.roles.active.background, + }), + + description: (theme) => ({ + fontSize: 13, + color: theme.palette.text.secondary, + lineHeight: "1.6", + display: "block", + }), + + useButtonContainer: { + display: "flex", + gap: 12, + flexDirection: "column", + paddingTop: 24, + marginTop: "auto", + alignItems: "center", + }, + + actionButton: (theme) => ({ + transition: "none", + color: theme.palette.text.secondary, + "&:hover": { + borderColor: theme.palette.text.primary, + }, + }), +} satisfies Record>; diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index 948ba5cfa890c..f1dacb53bad39 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -22,7 +22,6 @@ export const TemplatesPage: FC = () => { const error = templatesQuery.error || examplesQuery.error || organizationsQuery.error; const multiOrgExperimentEnabled = experiments.includes("multi-organization"); - console.log({ multiOrgExperimentEnabled }) return ( <> diff --git a/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx b/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx index 3c4a6982e838e..1cddbf34ede21 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx @@ -21,24 +21,12 @@ import { } from "components/PageHeader/PageHeader"; import { Stack } from "components/Stack/Stack"; // import { createDayString } from "utils/createDayString"; +import { TemplateCard } from "modules/templates/TemplateCard/TemplateCard"; import { docs } from "utils/docs"; -import { - formatTemplateBuildTime, - formatTemplateActiveDevelopers, -} from "utils/templates"; import { CreateTemplateButton } from "./CreateTemplateButton"; import { EmptyTemplates } from "./EmptyTemplates"; export const Language = { - developerCount: (activeCount: number): string => { - return `${formatTemplateActiveDevelopers(activeCount)} developer${ - activeCount !== 1 ? "s" : "" - }`; - }, - nameLabel: "Name", - buildTimeLabel: "Build time", - usedByLabel: "Used by", - lastUpdatedLabel: "Last updated", templateTooltipTitle: "What is template?", templateTooltipText: "With templates you can create a common configuration for your workspaces using Terraform.", @@ -143,7 +131,13 @@ export const TemplatesPageViewV2: FC = ({ /> ) : (templates && templates.map((template) => ( -

{template.name}

+ ({ + backgroundColor: theme.palette.background.paper, + })} + template={template} + key={template.id} + /> )))} From 51ebb6793a619f4246f79f8b58a48b65d3fc62fb Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 3 Jul 2024 19:40:56 +0000 Subject: [PATCH 03/25] feat: add component stories --- .../TemplateCard/TemplateCard.stories.tsx | 39 +++ .../TemplatesPageView.stories.tsx | 97 ++++++ .../TemplatesPageView.tsx | 195 ++++++++++++ .../TemplatesPageView.stories.tsx | 97 ++++++ .../TemplatePage/TemplatesPageView.tsx | 281 ++++++++++++++++++ 5 files changed, 709 insertions(+) create mode 100644 site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx create mode 100644 site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx create mode 100644 site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx create mode 100644 site/src/pages/TemplatesPage/TemplatePage/TemplatesPageView.stories.tsx create mode 100644 site/src/pages/TemplatesPage/TemplatePage/TemplatesPageView.tsx diff --git a/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx b/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx new file mode 100644 index 0000000000000..491b415757317 --- /dev/null +++ b/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { chromatic } from "testHelpers/chromatic"; +import { + MockTemplate, +} from "testHelpers/entities"; +import { TemplateCard } from "./TemplateCard"; + +const meta: Meta = { + title: "modules/templates/TemplateCard", + parameters: { chromatic }, + component: TemplateCard, + args: { + template: MockTemplate, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Template: Story = {}; + +export const DeprecatedTemplate: Story = { args: { + template: { + ...MockTemplate, + deprecated: true + } +},}; + +export const LongContentTemplate: Story = { + args: { + template: { + ...MockTemplate, + display_name: 'Very Long Template Name', + organization_name: 'Very Long Organization Name', + description: 'This is a very long test description. This is a very long test description. This is a very long test description. This is a very long test description', + active_user_count: 999 + } + }, +}; diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx new file mode 100644 index 0000000000000..ad9fede3cd611 --- /dev/null +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx @@ -0,0 +1,97 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { chromaticWithTablet } from "testHelpers/chromatic"; +import { + mockApiError, + MockTemplate, + MockTemplateExample, + MockTemplateExample2, +} from "testHelpers/entities"; +import { TemplatesPageView } from "./TemplatesPageView"; + +const meta: Meta = { + title: "pages/MultiOrgTemplatesPage", + parameters: { chromatic: chromaticWithTablet }, + component: TemplatesPageView, +}; + +export default meta; +type Story = StoryObj; + +export const WithTemplates: Story = { + args: { + 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. ", + }, + { + ...MockTemplate, + name: "template-without-icon", + display_name: "No Icon", + description: "This one has no icon", + icon: "", + }, + { + ...MockTemplate, + name: "template-without-icon-deprecated", + display_name: "Deprecated No Icon", + description: "This one has no icon and is deprecated", + deprecated: true, + deprecation_message: "This template is so old, it's deprecated", + icon: "", + }, + { + ...MockTemplate, + name: "deprecated-template", + display_name: "Deprecated", + description: "Template is incompatible", + }, + ], + examples: [], + }, +}; + +export const EmptyCanCreate: Story = { + args: { + canCreateTemplates: true, + error: undefined, + templates: [], + examples: [MockTemplateExample, MockTemplateExample2], + }, +}; + +export const EmptyCannotCreate: Story = { + args: { + error: undefined, + templates: [], + examples: [MockTemplateExample, MockTemplateExample2], + canCreateTemplates: false, + }, +}; + +export const Error: Story = { + args: { + error: mockApiError({ + message: "Something went wrong fetching templates.", + }), + templates: undefined, + examples: undefined, + canCreateTemplates: false, + }, +}; diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx new file mode 100644 index 0000000000000..e87dbb5f60587 --- /dev/null +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -0,0 +1,195 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import type { FC } from "react"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import type { Template, TemplateExample } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { + HelpTooltip, + HelpTooltipContent, + HelpTooltipLink, + HelpTooltipLinksGroup, + HelpTooltipText, + HelpTooltipTitle, + HelpTooltipTrigger, +} from "components/HelpTooltip/HelpTooltip"; +import { Margins } from "components/Margins/Margins"; +import { + PageHeader, + PageHeaderSubtitle, + PageHeaderTitle, +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { TemplateCard } from "modules/templates/TemplateCard/TemplateCard"; +import { docs } from "utils/docs"; +import { CreateTemplateButton } from "../CreateTemplateButton"; +import { EmptyTemplates } from "../EmptyTemplates"; + +export const Language = { + templateTooltipTitle: "What is template?", + templateTooltipText: + "With templates you can create a common configuration for your workspaces using Terraform.", + templateTooltipLink: "Manage templates", +}; + +const TemplateHelpTooltip: FC = () => { + return ( + + + + {Language.templateTooltipTitle} + {Language.templateTooltipText} + + + {Language.templateTooltipLink} + + + + + ); +}; + +export interface TemplatesPageViewProps { + templates: Template[] | undefined; + examples: TemplateExample[] | undefined; + canCreateTemplates: boolean; + error?: unknown; +} + +export type TemplatesByOrg = Record; + +const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { + const orgs: TemplatesByOrg = { + all: 0 + } + + templates.forEach((template) => { + if (orgs[template.organization_name]) { + orgs[template.organization_name] += 1 + } else { + orgs[template.organization_name] = 1; + } + + orgs.all += 1; + }) + + return orgs; +} + +export const TemplatesPageView: FC = ({ + templates, + examples, + canCreateTemplates, + error, +}) => { + const [urlParams] = useSearchParams(); + const isEmpty = templates && templates.length === 0; + const navigate = useNavigate(); + + const activeOrg = urlParams.get("org") ?? "all"; + + const templatesByOrg = getTemplatesByOrg(templates ?? []); + + return ( + + + } + > + + + Templates + + + + {templates && templates.length > 0 && ( + + Select a template to create a workspace. + + )} + + + {Boolean(error) && } + + + + ORGANIZATION + {Object.keys(templatesByOrg).map((org) => ( + + {org === 'all' ? 'All Organizations' : org} ({templatesByOrg[org] ?? 0}) + + ))} + + + +
+ {isEmpty ? ( + + ) : (templates && + templates.map((template) => ( + ({ + backgroundColor: theme.palette.background.paper, + })} + template={template} + key={template.id} + /> + )))} +
+
+
+ ); +}; + +const styles = { + filterCaption: (theme) => ({ + textTransform: "uppercase", + fontWeight: 600, + fontSize: 12, + color: theme.palette.text.secondary, + letterSpacing: "0.1em", + }), + tagLink: (theme) => ({ + color: theme.palette.text.secondary, + textDecoration: "none", + fontSize: 14, + textTransform: "capitalize", + + "&:hover": { + color: theme.palette.text.primary, + }, + }), + tagLinkActive: (theme) => ({ + color: theme.palette.text.primary, + fontWeight: 600, + }), + secondary: (theme) => ({ + color: theme.palette.text.secondary, + }), + actionButton: (theme) => ({ + transition: "none", + color: theme.palette.text.secondary, + "&:hover": { + borderColor: theme.palette.text.primary, + }, + }), +} satisfies Record>; diff --git a/site/src/pages/TemplatesPage/TemplatePage/TemplatesPageView.stories.tsx b/site/src/pages/TemplatesPage/TemplatePage/TemplatesPageView.stories.tsx new file mode 100644 index 0000000000000..07aecdb173ae9 --- /dev/null +++ b/site/src/pages/TemplatesPage/TemplatePage/TemplatesPageView.stories.tsx @@ -0,0 +1,97 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { chromaticWithTablet } from "testHelpers/chromatic"; +import { + mockApiError, + MockTemplate, + MockTemplateExample, + MockTemplateExample2, +} from "testHelpers/entities"; +import { TemplatesPageView } from "./TemplatesPageView"; + +const meta: Meta = { + title: "pages/TemplatesPage", + parameters: { chromatic: chromaticWithTablet }, + component: TemplatesPageView, +}; + +export default meta; +type Story = StoryObj; + +export const WithTemplates: Story = { + args: { + 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. ", + }, + { + ...MockTemplate, + name: "template-without-icon", + display_name: "No Icon", + description: "This one has no icon", + icon: "", + }, + { + ...MockTemplate, + name: "template-without-icon-deprecated", + display_name: "Deprecated No Icon", + description: "This one has no icon and is deprecated", + deprecated: true, + deprecation_message: "This template is so old, it's deprecated", + icon: "", + }, + { + ...MockTemplate, + name: "deprecated-template", + display_name: "Deprecated", + description: "Template is incompatible", + }, + ], + examples: [], + }, +}; + +export const EmptyCanCreate: Story = { + args: { + canCreateTemplates: true, + error: undefined, + templates: [], + examples: [MockTemplateExample, MockTemplateExample2], + }, +}; + +export const EmptyCannotCreate: Story = { + args: { + error: undefined, + templates: [], + examples: [MockTemplateExample, MockTemplateExample2], + canCreateTemplates: false, + }, +}; + +export const Error: Story = { + args: { + error: mockApiError({ + message: "Something went wrong fetching templates.", + }), + templates: undefined, + examples: undefined, + canCreateTemplates: false, + }, +}; diff --git a/site/src/pages/TemplatesPage/TemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatePage/TemplatesPageView.tsx new file mode 100644 index 0000000000000..7cf4d968f8e28 --- /dev/null +++ b/site/src/pages/TemplatesPage/TemplatePage/TemplatesPageView.tsx @@ -0,0 +1,281 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"; +import Button from "@mui/material/Button"; +import Skeleton from "@mui/material/Skeleton"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import type { FC } from "react"; +import { useNavigate } from "react-router-dom"; +import type { Template, TemplateExample } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { ExternalAvatar } from "components/Avatar/Avatar"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; +import { DeprecatedBadge } from "components/Badges/Badges"; +import { + HelpTooltip, + HelpTooltipContent, + HelpTooltipLink, + HelpTooltipLinksGroup, + HelpTooltipText, + HelpTooltipTitle, + HelpTooltipTrigger, +} from "components/HelpTooltip/HelpTooltip"; +import { Margins } from "components/Margins/Margins"; +import { + PageHeader, + PageHeaderSubtitle, + PageHeaderTitle, +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { + TableLoaderSkeleton, + TableRowSkeleton, +} from "components/TableLoader/TableLoader"; +import { useClickableTableRow } from "hooks/useClickableTableRow"; +import { createDayString } from "utils/createDayString"; +import { docs } from "utils/docs"; +import { + formatTemplateBuildTime, + formatTemplateActiveDevelopers, +} from "utils/templates"; +import { CreateTemplateButton } from "../CreateTemplateButton"; +import { EmptyTemplates } from "../EmptyTemplates"; + +export const Language = { + developerCount: (activeCount: number): string => { + return `${formatTemplateActiveDevelopers(activeCount)} developer${ + activeCount !== 1 ? "s" : "" + }`; + }, + nameLabel: "Name", + buildTimeLabel: "Build time", + usedByLabel: "Used by", + lastUpdatedLabel: "Last updated", + templateTooltipTitle: "What is template?", + templateTooltipText: + "With templates you can create a common configuration for your workspaces using Terraform.", + templateTooltipLink: "Manage templates", +}; + +const TemplateHelpTooltip: FC = () => { + return ( + + + + {Language.templateTooltipTitle} + {Language.templateTooltipText} + + + {Language.templateTooltipLink} + + + + + ); +}; + +interface TemplateRowProps { + template: Template; +} + +const TemplateRow: FC = ({ template }) => { + const templatePageLink = `/templates/${template.name}`; + const hasIcon = template.icon && template.icon !== ""; + const navigate = useNavigate(); + + const { css: clickableCss, ...clickableRow } = useClickableTableRow({ + onClick: () => navigate(templatePageLink), + }); + + return ( + + + 0 + ? template.display_name + : template.name + } + subtitle={template.description} + avatar={ + hasIcon && ( + + ) + } + /> + + + + {Language.developerCount(template.active_user_count)} + + + + {formatTemplateBuildTime(template.build_time_stats.start.P50)} + + + + {createDayString(template.updated_at)} + + + + {template.deprecated ? ( + + ) : ( + + )} + + + ); +}; + +export interface TemplatesPageViewProps { + error?: unknown; + examples: TemplateExample[] | undefined; + templates: Template[] | undefined; + canCreateTemplates: boolean; +} + +export const TemplatesPageView: FC = ({ + templates, + error, + examples, + canCreateTemplates, +}) => { + const isLoading = !templates; + const isEmpty = templates && templates.length === 0; + const navigate = useNavigate(); + + return ( + + + } + > + + + Templates + + + + {templates && templates.length > 0 && ( + + Select a template to create a workspace. + + )} + + + {error ? ( + + ) : ( + + + + + {Language.nameLabel} + {Language.usedByLabel} + {Language.buildTimeLabel} + {Language.lastUpdatedLabel} + + + + + {isLoading && } + + {isEmpty ? ( + + ) : ( + templates?.map((template) => ( + + )) + )} + +
+
+ )} +
+ ); +}; + +const TableLoader: FC = () => { + return ( + + + +
+ +
+
+ + + + + + + + + + + + +
+
+ ); +}; + +const styles = { + templateIconWrapper: { + // Same size then the avatar component + width: 36, + height: 36, + padding: 2, + + "& img": { + width: "100%", + }, + }, + actionCell: { + whiteSpace: "nowrap", + }, + secondary: (theme) => ({ + color: theme.palette.text.secondary, + }), + tableRow: (theme) => ({ + "&:hover .actionButton": { + color: theme.experimental.l2.hover.text, + borderColor: theme.experimental.l2.hover.outline, + }, + }), + actionButton: (theme) => ({ + transition: "none", + color: theme.palette.text.secondary, + "&:hover": { + borderColor: theme.palette.text.primary, + }, + }), +} satisfies Record>; From a0effc6dc9a8dcdfc26cf15c0c5ddb5303ebe115 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 3 Jul 2024 19:52:54 +0000 Subject: [PATCH 04/25] chore: update template query naming --- site/src/api/api.ts | 21 +- site/src/api/queries/templates.ts | 22 +- .../src/pages/TemplatesPage/TemplatesPage.tsx | 18 +- .../TemplatesPageView.stories.tsx | 97 ------ .../pages/TemplatesPage/TemplatesPageView.tsx | 281 ------------------ .../TemplatesPage/TemplatesPageViewV2.tsx | 180 ----------- .../pages/WorkspacesPage/WorkspacesPage.tsx | 4 +- .../src/pages/WorkspacesPage/filter/menus.tsx | 4 +- 8 files changed, 49 insertions(+), 578 deletions(-) delete mode 100644 site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx delete mode 100644 site/src/pages/TemplatesPage/TemplatesPageView.tsx delete mode 100644 site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index e60da675ccaa9..dad528113ca55 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -578,7 +578,7 @@ class ApiMethods { return response.data; }; - getTemplates = async ( + getTemplatesByOrganizationId = async ( organizationId: string, options?: TemplateOptions, ): Promise => { @@ -598,6 +598,25 @@ class ApiMethods { return response.data; }; + getTemplates = async ( + options?: TemplateOptions, + ): Promise => { + const params: Record = {}; + if (options?.deprecated !== undefined) { + // Just want to check if it isn't undefined. If it has + // a boolean value, convert it to a string and include + // it as a param. + params["deprecated"] = String(options.deprecated); + } + + const response = await this.axios.get( + `/api/v2/templates`, + { params }, + ); + + return response.data; + }; + getTemplateByName = async ( organizationId: string, name: string, diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 2d0485b8f347b..0c25e59cc93f5 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -30,16 +30,28 @@ export const templateByName = ( }; }; -const getTemplatesQueryKey = (organizationId: string, deprecated?: boolean) => [ +const getTemplatesByOrganizationIdQueryKey = (organizationId: string, deprecated?: boolean) => [ organizationId, "templates", deprecated, ]; -export const templates = (organizationId: string, deprecated?: boolean) => { +export const templatesByOrganizationId = (organizationId: string, deprecated?: boolean) => { return { - queryKey: getTemplatesQueryKey(organizationId, deprecated), - queryFn: () => API.getTemplates(organizationId, { deprecated }), + queryKey: getTemplatesByOrganizationIdQueryKey(organizationId, deprecated), + queryFn: () => API.getTemplatesByOrganizationId(organizationId, { deprecated }), + }; +}; + +const getTemplatesQueryKey = (deprecated?: boolean) => [ + "templates", + deprecated, +]; + +export const templates = (deprecated?: boolean) => { + return { + queryKey: getTemplatesQueryKey(deprecated), + queryFn: () => API.getTemplates({ deprecated }), }; }; @@ -92,7 +104,7 @@ export const setGroupRole = ( export const templateExamples = (organizationId: string) => { return { - queryKey: [...getTemplatesQueryKey(organizationId), "examples"], + queryKey: [...getTemplatesByOrganizationIdQueryKey(organizationId), "examples"], queryFn: () => API.getTemplateExamples(organizationId), }; }; diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index f1dacb53bad39..c08fa6b03f5bb 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -1,25 +1,24 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; -import { templateExamples, templates } from "api/queries/templates"; -import { myOrganizations } from "api/queries/users"; +import { templateExamples, templatesByOrganizationId, templates, } from "api/queries/templates"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useDashboard } from "modules/dashboard/useDashboard"; import { pageTitle } from "utils/page"; -import { TemplatesPageView } from "./TemplatesPageView"; -import { TemplatesPageViewV2 } from "./TemplatesPageViewV2"; +import { TemplatesPageView as MultiOrgTemplatesPageView } from "./MultiOrgTemplatePage/TemplatesPageView"; +import { TemplatesPageView } from "./TemplatePage/TemplatesPageView"; export const TemplatesPage: FC = () => { const { permissions } = useAuthenticated(); const { organizationId, experiments } = useDashboard(); - const organizationsQuery = useQuery(myOrganizations()); - const templatesQuery = useQuery(templates(organizationId)); + const templatesByOrganizationIdQuery = useQuery(templatesByOrganizationId(organizationId)); + const templatesQuery = useQuery(templates()); const examplesQuery = useQuery({ ...templateExamples(organizationId), enabled: permissions.createTemplates, }); - const error = templatesQuery.error || examplesQuery.error || organizationsQuery.error; + const error = templatesByOrganizationIdQuery.error || examplesQuery.error || templatesQuery.error; const multiOrgExperimentEnabled = experiments.includes("multi-organization"); return ( @@ -28,19 +27,18 @@ export const TemplatesPage: FC = () => { {pageTitle("Templates")}
{multiOrgExperimentEnabled ? ( - ) : ( )} diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx deleted file mode 100644 index 07aecdb173ae9..0000000000000 --- a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { chromaticWithTablet } from "testHelpers/chromatic"; -import { - mockApiError, - MockTemplate, - MockTemplateExample, - MockTemplateExample2, -} from "testHelpers/entities"; -import { TemplatesPageView } from "./TemplatesPageView"; - -const meta: Meta = { - title: "pages/TemplatesPage", - parameters: { chromatic: chromaticWithTablet }, - component: TemplatesPageView, -}; - -export default meta; -type Story = StoryObj; - -export const WithTemplates: Story = { - args: { - 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. ", - }, - { - ...MockTemplate, - name: "template-without-icon", - display_name: "No Icon", - description: "This one has no icon", - icon: "", - }, - { - ...MockTemplate, - name: "template-without-icon-deprecated", - display_name: "Deprecated No Icon", - description: "This one has no icon and is deprecated", - deprecated: true, - deprecation_message: "This template is so old, it's deprecated", - icon: "", - }, - { - ...MockTemplate, - name: "deprecated-template", - display_name: "Deprecated", - description: "Template is incompatible", - }, - ], - examples: [], - }, -}; - -export const EmptyCanCreate: Story = { - args: { - canCreateTemplates: true, - error: undefined, - templates: [], - examples: [MockTemplateExample, MockTemplateExample2], - }, -}; - -export const EmptyCannotCreate: Story = { - args: { - error: undefined, - templates: [], - examples: [MockTemplateExample, MockTemplateExample2], - canCreateTemplates: false, - }, -}; - -export const Error: Story = { - args: { - error: mockApiError({ - message: "Something went wrong fetching templates.", - }), - templates: undefined, - examples: undefined, - canCreateTemplates: false, - }, -}; diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx deleted file mode 100644 index fd7be676da6cb..0000000000000 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"; -import Button from "@mui/material/Button"; -import Skeleton from "@mui/material/Skeleton"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import type { FC } from "react"; -import { useNavigate } from "react-router-dom"; -import type { Template, TemplateExample } from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { ExternalAvatar } from "components/Avatar/Avatar"; -import { AvatarData } from "components/AvatarData/AvatarData"; -import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; -import { DeprecatedBadge } from "components/Badges/Badges"; -import { - HelpTooltip, - HelpTooltipContent, - HelpTooltipLink, - HelpTooltipLinksGroup, - HelpTooltipText, - HelpTooltipTitle, - HelpTooltipTrigger, -} from "components/HelpTooltip/HelpTooltip"; -import { Margins } from "components/Margins/Margins"; -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, -} from "components/PageHeader/PageHeader"; -import { Stack } from "components/Stack/Stack"; -import { - TableLoaderSkeleton, - TableRowSkeleton, -} from "components/TableLoader/TableLoader"; -import { useClickableTableRow } from "hooks/useClickableTableRow"; -import { createDayString } from "utils/createDayString"; -import { docs } from "utils/docs"; -import { - formatTemplateBuildTime, - formatTemplateActiveDevelopers, -} from "utils/templates"; -import { CreateTemplateButton } from "./CreateTemplateButton"; -import { EmptyTemplates } from "./EmptyTemplates"; - -export const Language = { - developerCount: (activeCount: number): string => { - return `${formatTemplateActiveDevelopers(activeCount)} developer${ - activeCount !== 1 ? "s" : "" - }`; - }, - nameLabel: "Name", - buildTimeLabel: "Build time", - usedByLabel: "Used by", - lastUpdatedLabel: "Last updated", - templateTooltipTitle: "What is template?", - templateTooltipText: - "With templates you can create a common configuration for your workspaces using Terraform.", - templateTooltipLink: "Manage templates", -}; - -const TemplateHelpTooltip: FC = () => { - return ( - - - - {Language.templateTooltipTitle} - {Language.templateTooltipText} - - - {Language.templateTooltipLink} - - - - - ); -}; - -interface TemplateRowProps { - template: Template; -} - -const TemplateRow: FC = ({ template }) => { - const templatePageLink = `/templates/${template.name}`; - const hasIcon = template.icon && template.icon !== ""; - const navigate = useNavigate(); - - const { css: clickableCss, ...clickableRow } = useClickableTableRow({ - onClick: () => navigate(templatePageLink), - }); - - return ( - - - 0 - ? template.display_name - : template.name - } - subtitle={template.description} - avatar={ - hasIcon && ( - - ) - } - /> - - - - {Language.developerCount(template.active_user_count)} - - - - {formatTemplateBuildTime(template.build_time_stats.start.P50)} - - - - {createDayString(template.updated_at)} - - - - {template.deprecated ? ( - - ) : ( - - )} - - - ); -}; - -export interface TemplatesPageViewProps { - error?: unknown; - examples: TemplateExample[] | undefined; - templates: Template[] | undefined; - canCreateTemplates: boolean; -} - -export const TemplatesPageView: FC = ({ - templates, - error, - examples, - canCreateTemplates, -}) => { - const isLoading = !templates; - const isEmpty = templates && templates.length === 0; - const navigate = useNavigate(); - - return ( - - - } - > - - - Templates - - - - {templates && templates.length > 0 && ( - - Select a template to create a workspace. - - )} - - - {error ? ( - - ) : ( - - - - - {Language.nameLabel} - {Language.usedByLabel} - {Language.buildTimeLabel} - {Language.lastUpdatedLabel} - - - - - {isLoading && } - - {isEmpty ? ( - - ) : ( - templates?.map((template) => ( - - )) - )} - -
-
- )} -
- ); -}; - -const TableLoader: FC = () => { - return ( - - - -
- -
-
- - - - - - - - - - - - -
-
- ); -}; - -const styles = { - templateIconWrapper: { - // Same size then the avatar component - width: 36, - height: 36, - padding: 2, - - "& img": { - width: "100%", - }, - }, - actionCell: { - whiteSpace: "nowrap", - }, - secondary: (theme) => ({ - color: theme.palette.text.secondary, - }), - tableRow: (theme) => ({ - "&:hover .actionButton": { - color: theme.experimental.l2.hover.text, - borderColor: theme.experimental.l2.hover.outline, - }, - }), - actionButton: (theme) => ({ - transition: "none", - color: theme.palette.text.secondary, - "&:hover": { - borderColor: theme.palette.text.primary, - }, - }), -} satisfies Record>; diff --git a/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx b/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx deleted file mode 100644 index 1cddbf34ede21..0000000000000 --- a/site/src/pages/TemplatesPage/TemplatesPageViewV2.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import type { FC } from "react"; -import { Link, useNavigate, useSearchParams } from "react-router-dom"; -import type { Organization, Template, TemplateExample } from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { - HelpTooltip, - HelpTooltipContent, - HelpTooltipLink, - HelpTooltipLinksGroup, - HelpTooltipText, - HelpTooltipTitle, - HelpTooltipTrigger, -} from "components/HelpTooltip/HelpTooltip"; -// import { Loader } from "components/Loader/Loader"; -import { Margins } from "components/Margins/Margins"; -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, -} from "components/PageHeader/PageHeader"; -import { Stack } from "components/Stack/Stack"; -// import { createDayString } from "utils/createDayString"; -import { TemplateCard } from "modules/templates/TemplateCard/TemplateCard"; -import { docs } from "utils/docs"; -import { CreateTemplateButton } from "./CreateTemplateButton"; -import { EmptyTemplates } from "./EmptyTemplates"; - -export const Language = { - templateTooltipTitle: "What is template?", - templateTooltipText: - "With templates you can create a common configuration for your workspaces using Terraform.", - templateTooltipLink: "Manage templates", -}; - -const TemplateHelpTooltip: FC = () => { - return ( - - - - {Language.templateTooltipTitle} - {Language.templateTooltipText} - - - {Language.templateTooltipLink} - - - - - ); -}; - -export interface TemplatesPageViewProps { - templates: Template[] | undefined; - organizations: Organization[] | undefined; - examples: TemplateExample[] | undefined; - canCreateTemplates: boolean; - error?: unknown; -} - -export const TemplatesPageViewV2: FC = ({ - templates, - organizations, - examples, - canCreateTemplates, - error, -}) => { - const [urlParams] = useSearchParams(); - const isEmpty = templates && templates.length === 0; - const navigate = useNavigate(); - - const activeOrg = urlParams.get("org") ?? "all"; - - return ( - - - } - > - - - Templates - - - - {templates && templates.length > 0 && ( - - Select a template to create a workspace. - - )} - - - {Boolean(error) && } - - {/* {Boolean(isLoading) && } */} - - - - ORGANIZATION - {organizations?.map((org) => ( - - {/* {org.name} ({starterTemplatesByTag[tag].length}) */} - {org.name} (1) - - ))} - - - -
- {isEmpty ? ( - - ) : (templates && - templates.map((template) => ( - ({ - backgroundColor: theme.palette.background.paper, - })} - template={template} - key={template.id} - /> - )))} -
-
-
- ); -}; - -const styles = { - filterCaption: (theme) => ({ - textTransform: "uppercase", - fontWeight: 600, - fontSize: 12, - color: theme.palette.text.secondary, - letterSpacing: "0.1em", - }), - tagLink: (theme) => ({ - color: theme.palette.text.secondary, - textDecoration: "none", - fontSize: 14, - textTransform: "capitalize", - - "&:hover": { - color: theme.palette.text.primary, - }, - }), - tagLinkActive: (theme) => ({ - color: theme.palette.text.primary, - fontWeight: 600, - }), - secondary: (theme) => ({ - color: theme.palette.text.secondary, - }), - actionButton: (theme) => ({ - transition: "none", - color: theme.palette.text.secondary, - "&:hover": { - borderColor: theme.palette.text.primary, - }, - }), -} satisfies Record>; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 277716f6a959c..7db2fee7cff94 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -2,7 +2,7 @@ import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { useSearchParams } from "react-router-dom"; -import { templates } from "api/queries/templates"; +import { templatesByOrganizationId } from "api/queries/templates"; import type { Workspace } from "api/typesGenerated"; import { useFilter } from "components/Filter/filter"; import { useUserFilterMenu } from "components/Filter/UserFilter"; @@ -41,7 +41,7 @@ const WorkspacesPage: FC = () => { const { permissions } = useAuthenticated(); const { entitlements, organizationId } = useDashboard(); - const templatesQuery = useQuery(templates(organizationId, false)); + const templatesQuery = useQuery(templatesByOrganizationId(organizationId, false)); const filterProps = useWorkspacesFilter({ searchParamsResult, diff --git a/site/src/pages/WorkspacesPage/filter/menus.tsx b/site/src/pages/WorkspacesPage/filter/menus.tsx index 0316f158e87c9..1ef95002ab404 100644 --- a/site/src/pages/WorkspacesPage/filter/menus.tsx +++ b/site/src/pages/WorkspacesPage/filter/menus.tsx @@ -27,7 +27,7 @@ export const useTemplateFilterMenu = ({ id: "template", getSelectedOption: async () => { // Show all templates including deprecated - const templates = await API.getTemplates(organizationId); + const templates = await API.getTemplatesByOrganizationId(organizationId); const template = templates.find((template) => template.name === value); if (template) { return { @@ -40,7 +40,7 @@ export const useTemplateFilterMenu = ({ }, getOptions: async (query) => { // Show all templates including deprecated - const templates = await API.getTemplates(organizationId); + const templates = await API.getTemplatesByOrganizationId(organizationId); const filteredTemplates = templates.filter( (template) => template.name.toLowerCase().includes(query.toLowerCase()) || From be3708598b58551ae89e4528ff8264465047f3d2 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 3 Jul 2024 20:21:28 +0000 Subject: [PATCH 05/25] fix: fix formatting --- site/src/api/queries/templates.ts | 22 +++--- .../TemplateCard/TemplateCard.stories.tsx | 29 ++++---- .../templates/TemplateCard/TemplateCard.tsx | 25 +++---- .../TemplatesPageView.tsx | 71 +++++++++---------- .../src/pages/TemplatesPage/TemplatesPage.tsx | 19 +++-- .../pages/WorkspacesPage/WorkspacesPage.tsx | 4 +- 6 files changed, 93 insertions(+), 77 deletions(-) diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 0c25e59cc93f5..42e519645fe41 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -30,16 +30,19 @@ export const templateByName = ( }; }; -const getTemplatesByOrganizationIdQueryKey = (organizationId: string, deprecated?: boolean) => [ - organizationId, - "templates", - deprecated, -]; +const getTemplatesByOrganizationIdQueryKey = ( + organizationId: string, + deprecated?: boolean, +) => [organizationId, "templates", deprecated]; -export const templatesByOrganizationId = (organizationId: string, deprecated?: boolean) => { +export const templatesByOrganizationId = ( + organizationId: string, + deprecated?: boolean, +) => { return { queryKey: getTemplatesByOrganizationIdQueryKey(organizationId, deprecated), - queryFn: () => API.getTemplatesByOrganizationId(organizationId, { deprecated }), + queryFn: () => + API.getTemplatesByOrganizationId(organizationId, { deprecated }), }; }; @@ -104,7 +107,10 @@ export const setGroupRole = ( export const templateExamples = (organizationId: string) => { return { - queryKey: [...getTemplatesByOrganizationIdQueryKey(organizationId), "examples"], + queryKey: [ + ...getTemplatesByOrganizationIdQueryKey(organizationId), + "examples", + ], queryFn: () => API.getTemplateExamples(organizationId), }; }; diff --git a/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx b/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx index 491b415757317..7b55d2c0c8275 100644 --- a/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx +++ b/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx @@ -1,8 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { chromatic } from "testHelpers/chromatic"; -import { - MockTemplate, -} from "testHelpers/entities"; +import { MockTemplate } from "testHelpers/entities"; import { TemplateCard } from "./TemplateCard"; const meta: Meta = { @@ -19,21 +17,24 @@ type Story = StoryObj; export const Template: Story = {}; -export const DeprecatedTemplate: Story = { args: { - template: { - ...MockTemplate, - deprecated: true - } -},}; +export const DeprecatedTemplate: Story = { + args: { + template: { + ...MockTemplate, + deprecated: true, + }, + }, +}; export const LongContentTemplate: Story = { args: { template: { ...MockTemplate, - display_name: 'Very Long Template Name', - organization_name: 'Very Long Organization Name', - description: 'This is a very long test description. This is a very long test description. This is a very long test description. This is a very long test description', - active_user_count: 999 - } + display_name: "Very Long Template Name", + organization_name: "Very Long Organization Name", + description: + "This is a very long test description. This is a very long test description. This is a very long test description. This is a very long test description", + active_user_count: 999, + }, }, }; diff --git a/site/src/modules/templates/TemplateCard/TemplateCard.tsx b/site/src/modules/templates/TemplateCard/TemplateCard.tsx index b1868b34a33c2..e936892e6cdd2 100644 --- a/site/src/modules/templates/TemplateCard/TemplateCard.tsx +++ b/site/src/modules/templates/TemplateCard/TemplateCard.tsx @@ -21,7 +21,7 @@ export const TemplateCard: FC = ({ return (
-
+
0 @@ -37,7 +37,8 @@ export const TemplateCard: FC = ({ />
- {template.active_user_count} {template.active_user_count === 1 ? "user" : "users" } + {template.active_user_count}{" "} + {template.active_user_count === 1 ? "user" : "users"}
@@ -52,16 +53,16 @@ export const TemplateCard: FC = ({ ) : ( + component={RouterLink} + css={styles.actionButton} + className="actionButton" + fullWidth + startIcon={} + title={`Create a workspace using the ${template.display_name} template`} + to={`/templates/${template.name}/workspace`} + > + Create Workspace + )}
diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index e87dbb5f60587..d9147b28b3845 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -59,21 +59,21 @@ export type TemplatesByOrg = Record; const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { const orgs: TemplatesByOrg = { - all: 0 - } + all: 0, + }; templates.forEach((template) => { if (orgs[template.organization_name]) { - orgs[template.organization_name] += 1 + orgs[template.organization_name] += 1; } else { orgs[template.organization_name] = 1; } orgs.all += 1; - }) + }); return orgs; -} +}; export const TemplatesPageView: FC = ({ templates, @@ -112,24 +112,19 @@ export const TemplatesPageView: FC = ({ {Boolean(error) && } - - ORGANIZATION - {Object.keys(templatesByOrg).map((org) => ( - - {org === 'all' ? 'All Organizations' : org} ({templatesByOrg[org] ?? 0}) - - ))} - - + + ORGANIZATION + {Object.keys(templatesByOrg).map((org) => ( + + {org === "all" ? "All Organizations" : org} ( + {templatesByOrg[org] ?? 0}) + + ))} +
= ({ }} > {isEmpty ? ( - - ) : (templates && - templates.map((template) => ( - ({ - backgroundColor: theme.palette.background.paper, - })} - template={template} - key={template.id} - /> - )))} + + ) : ( + templates && + templates.map((template) => ( + ({ + backgroundColor: theme.palette.background.paper, + })} + template={template} + key={template.id} + /> + )) + )}
diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index c08fa6b03f5bb..3db3afc46a20b 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -1,7 +1,11 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; -import { templateExamples, templatesByOrganizationId, templates, } from "api/queries/templates"; +import { + templateExamples, + templatesByOrganizationId, + templates, +} from "api/queries/templates"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useDashboard } from "modules/dashboard/useDashboard"; import { pageTitle } from "utils/page"; @@ -12,13 +16,18 @@ export const TemplatesPage: FC = () => { const { permissions } = useAuthenticated(); const { organizationId, experiments } = useDashboard(); - const templatesByOrganizationIdQuery = useQuery(templatesByOrganizationId(organizationId)); + const templatesByOrganizationIdQuery = useQuery( + templatesByOrganizationId(organizationId), + ); const templatesQuery = useQuery(templates()); const examplesQuery = useQuery({ ...templateExamples(organizationId), enabled: permissions.createTemplates, }); - const error = templatesByOrganizationIdQuery.error || examplesQuery.error || templatesQuery.error; + const error = + templatesByOrganizationIdQuery.error || + examplesQuery.error || + templatesQuery.error; const multiOrgExperimentEnabled = experiments.includes("multi-organization"); return ( @@ -33,14 +42,14 @@ export const TemplatesPage: FC = () => { examples={examplesQuery.data} templates={templatesQuery.data} /> - ) : ( + ) : ( - )} + )} ); }; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 7db2fee7cff94..944e32580acaf 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -41,7 +41,9 @@ const WorkspacesPage: FC = () => { const { permissions } = useAuthenticated(); const { entitlements, organizationId } = useDashboard(); - const templatesQuery = useQuery(templatesByOrganizationId(organizationId, false)); + const templatesQuery = useQuery( + templatesByOrganizationId(organizationId, false), + ); const filterProps = useWorkspacesFilter({ searchParamsResult, From 56491669d11fada920d3fb59549ea479e4480e5a Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 8 Jul 2024 22:30:29 +0000 Subject: [PATCH 06/25] feat: template card interaction and navigation --- .../templates/TemplateCard/TemplateCard.tsx | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/site/src/modules/templates/TemplateCard/TemplateCard.tsx b/site/src/modules/templates/TemplateCard/TemplateCard.tsx index e936892e6cdd2..25421d5a4a910 100644 --- a/site/src/modules/templates/TemplateCard/TemplateCard.tsx +++ b/site/src/modules/templates/TemplateCard/TemplateCard.tsx @@ -2,7 +2,7 @@ import type { Interpolation, Theme } from "@emotion/react"; import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"; import Button from "@mui/material/Button"; import type { FC, HTMLAttributes } from "react"; -import { Link as RouterLink } from "react-router-dom"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; import type { Template } from "api/typesGenerated"; import { ExternalAvatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; @@ -16,10 +16,25 @@ export const TemplateCard: FC = ({ template, ...divProps }) => { + const navigate = useNavigate(); + const templatePageLink = `/templates/${template.name}`; const hasIcon = template.icon && template.icon !== ""; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && e.currentTarget === e.target) { + navigate(templatePageLink); + } + }; + return ( -
+
navigate(templatePageLink)} + onKeyDown={handleKeyDown} + >
= ({ startIcon={} title={`Create a workspace using the ${template.display_name} template`} to={`/templates/${template.name}/workspace`} + onClick={(e) => { + e.stopPropagation(); + }} > Create Workspace @@ -79,6 +97,11 @@ const styles = { color: "inherit", display: "flex", flexDirection: "column", + cursor: "pointer", + "&:hover": { + color: theme.experimental.l2.hover.text, + borderColor: theme.experimental.l2.hover.text, + }, }), header: { @@ -95,27 +118,6 @@ const styles = { height: 32, }, - tags: { - display: "flex", - flexWrap: "wrap", - gap: 8, - justifyContent: "end", - }, - - tag: (theme) => ({ - borderColor: theme.palette.divider, - textDecoration: "none", - cursor: "pointer", - "&: hover": { - borderColor: theme.palette.primary.main, - }, - }), - - activeTag: (theme) => ({ - borderColor: theme.roles.active.outline, - backgroundColor: theme.roles.active.background, - }), - description: (theme) => ({ fontSize: 13, color: theme.palette.text.secondary, From 3ea3aa2cbe10d0aff34818675e2d6f8a7aebeb6d Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 8 Jul 2024 22:40:10 +0000 Subject: [PATCH 07/25] fix: copy updates --- .../TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index d9147b28b3845..a8e3c3205d7d9 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -25,9 +25,9 @@ import { CreateTemplateButton } from "../CreateTemplateButton"; import { EmptyTemplates } from "../EmptyTemplates"; export const Language = { - templateTooltipTitle: "What is template?", + templateTooltipTitle: "What is a template?", templateTooltipText: - "With templates you can create a common configuration for your workspaces using Terraform.", + "Templates allow you to create a common configuration for your workspaces using Terraform.", templateTooltipLink: "Manage templates", }; From f17a0c39ab003cdfd7ac284c05060827f21a5a41 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 9 Jul 2024 21:27:47 +0000 Subject: [PATCH 08/25] chore: update TemplateFilter type to include FilterQuery --- codersdk/organizations.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 0841bdba8554f..d92b4e65f9883 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -367,7 +367,8 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui } type TemplateFilter struct { - OrganizationID uuid.UUID + OrganizationID uuid.UUID `json:"organization_id,omitempty" typescript:"-"` + FilterQuery string `json:"q,omitempty"` ExactName string } @@ -386,6 +387,11 @@ func (f TemplateFilter) asRequestOption() RequestOption { params = append(params, fmt.Sprintf("exact_name:%q", f.ExactName)) } + if f.FilterQuery != "" { + // If custom stuff is added, just add it on here. + params = append(params, f.FilterQuery) + } + q := r.URL.Query() q.Set("q", strings.Join(params, " ")) r.URL.RawQuery = q.Encode() From 0077db017899d32923f8edda3a46379a9b4258fa Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 9 Jul 2024 21:28:04 +0000 Subject: [PATCH 09/25] chore: update typesGenerated.ts --- site/src/api/typesGenerated.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 34b3285f1603b..2b7e32787b2cd 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1224,8 +1224,8 @@ export interface TemplateExample { // From codersdk/organizations.go export interface TemplateFilter { - readonly OrganizationID: string; readonly ExactName: string; + readonly q?: string; } // From codersdk/templates.go From 461202e2cff2db560bf197d837a855d7cb4fedda Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 9 Jul 2024 21:33:16 +0000 Subject: [PATCH 10/25] feat: update template filter api logic --- site/src/api/api.ts | 25 +++++-------------- site/src/api/queries/audits.ts | 4 +-- site/src/api/queries/templates.ts | 12 +++------ site/src/components/Filter/filter.tsx | 7 +++--- .../TemplatesPageView.tsx | 15 +++++------ .../src/pages/TemplatesPage/TemplatesPage.tsx | 7 +++++- site/src/utils/filters.ts | 1 + 7 files changed, 30 insertions(+), 41 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index dad528113ca55..49e7e8254528f 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -187,8 +187,7 @@ export const watchBuildLogsByTemplateVersionId = ( const proto = location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket( - `${proto}//${ - location.host + `${proto}//${location.host }/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`, ); @@ -270,8 +269,7 @@ export const watchBuildLogsByBuildId = ( } const proto = location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket( - `${proto}//${ - location.host + `${proto}//${location.host }/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`, ); socket.binaryType = "blob"; @@ -382,7 +380,7 @@ export class MissingBuildParameters extends Error { * lexical scope. */ class ApiMethods { - constructor(protected readonly axios: AxiosInstance) {} + constructor(protected readonly axios: AxiosInstance) { } login = async ( email: string, @@ -599,21 +597,10 @@ class ApiMethods { }; getTemplates = async ( - options?: TemplateOptions, + options?: TypesGen.TemplateFilter, ): Promise => { - const params: Record = {}; - if (options?.deprecated !== undefined) { - // Just want to check if it isn't undefined. If it has - // a boolean value, convert it to a string and include - // it as a param. - params["deprecated"] = String(options.deprecated); - } - - const response = await this.axios.get( - `/api/v2/templates`, - { params }, - ); - + const url = getURLWithSearchParams("/api/v2/templates", options); + const response = await this.axios.get(url); return response.data; }; diff --git a/site/src/api/queries/audits.ts b/site/src/api/queries/audits.ts index 1dce9a29eaab8..dbdfea48ff742 100644 --- a/site/src/api/queries/audits.ts +++ b/site/src/api/queries/audits.ts @@ -1,14 +1,14 @@ import { API } from "api/api"; import type { AuditLogResponse } from "api/typesGenerated"; -import { useFilterParamsKey } from "components/Filter/filter"; import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery"; +import { filterParamsKey } from "utils/filters"; export function paginatedAudits( searchParams: URLSearchParams, ): UsePaginatedQueryOptions { return { searchParams, - queryPayload: () => searchParams.get(useFilterParamsKey) ?? "", + queryPayload: () => searchParams.get(filterParamsKey) ?? "", queryKey: ({ payload, pageNumber }) => { return ["auditLogs", payload, pageNumber] as const; }, diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 42e519645fe41..862eb86c1f24e 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -1,6 +1,7 @@ import type { MutationOptions, QueryClient, QueryOptions } from "react-query"; import { API } from "api/api"; import type { + TemplateFilter, CreateTemplateRequest, CreateTemplateVersionRequest, ProvisionerJob, @@ -46,15 +47,10 @@ export const templatesByOrganizationId = ( }; }; -const getTemplatesQueryKey = (deprecated?: boolean) => [ - "templates", - deprecated, -]; - -export const templates = (deprecated?: boolean) => { +export const templates = (config: TemplateFilter = {}) => { return { - queryKey: getTemplatesQueryKey(deprecated), - queryFn: () => API.getTemplates({ deprecated }), + queryKey: ["templates", config], + queryFn: () => API.getTemplates(config), }; }; diff --git a/site/src/components/Filter/filter.tsx b/site/src/components/Filter/filter.tsx index b26ce444a805f..f37510dbd2a00 100644 --- a/site/src/components/Filter/filter.tsx +++ b/site/src/components/Filter/filter.tsx @@ -16,6 +16,7 @@ import { import { InputGroup } from "components/InputGroup/InputGroup"; import { SearchField } from "components/SearchField/SearchField"; import { useDebouncedFunction } from "hooks/debounce"; +import { filterParamsKey } from "utils/filters"; export type PresetFilter = { name: string; @@ -35,21 +36,19 @@ type UseFilterConfig = { onUpdate?: (newValue: string) => void; }; -export const useFilterParamsKey = "filter"; - export const useFilter = ({ fallbackFilter = "", searchParamsResult, onUpdate, }: UseFilterConfig) => { const [searchParams, setSearchParams] = searchParamsResult; - const query = searchParams.get(useFilterParamsKey) ?? fallbackFilter; + const query = searchParams.get(filterParamsKey) ?? fallbackFilter; const update = (newValues: string | FilterValues) => { const serialized = typeof newValues === "string" ? newValues : stringifyFilter(newValues); - searchParams.set(useFilterParamsKey, serialized); + searchParams.set(filterParamsKey, serialized); setSearchParams(searchParams); if (onUpdate !== undefined) { diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index a8e3c3205d7d9..9af52896fcaf6 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -52,6 +52,7 @@ export interface TemplatesPageViewProps { templates: Template[] | undefined; examples: TemplateExample[] | undefined; canCreateTemplates: boolean; + query: string | undefined; error?: unknown; } @@ -79,14 +80,12 @@ export const TemplatesPageView: FC = ({ templates, examples, canCreateTemplates, + query, error, }) => { - const [urlParams] = useSearchParams(); - const isEmpty = templates && templates.length === 0; const navigate = useNavigate(); - - const activeOrg = urlParams.get("org") ?? "all"; - + const isEmpty = templates && templates.length === 0; + const activeOrg = query?.split(":")[1] ?? "all"; const templatesByOrg = getTemplatesByOrg(templates ?? []); return ( @@ -109,7 +108,9 @@ export const TemplatesPageView: FC = ({ )} - {Boolean(error) && } + {Boolean(error) && ( + + )} @@ -117,7 +118,7 @@ export const TemplatesPageView: FC = ({ {Object.keys(templatesByOrg).map((org) => ( {org === "all" ? "All Organizations" : org} ( diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index 3db3afc46a20b..49f0dea1197c8 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -1,6 +1,7 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; +import { useSearchParams } from "react-router-dom"; import { templateExamples, templatesByOrganizationId, @@ -8,6 +9,7 @@ import { } from "api/queries/templates"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useDashboard } from "modules/dashboard/useDashboard"; +import { filterParamsKey } from "utils/filters"; import { pageTitle } from "utils/page"; import { TemplatesPageView as MultiOrgTemplatesPageView } from "./MultiOrgTemplatePage/TemplatesPageView"; import { TemplatesPageView } from "./TemplatePage/TemplatesPageView"; @@ -15,11 +17,13 @@ import { TemplatesPageView } from "./TemplatePage/TemplatesPageView"; export const TemplatesPage: FC = () => { const { permissions } = useAuthenticated(); const { organizationId, experiments } = useDashboard(); + const [searchParams] = useSearchParams(); + const query = searchParams.get(filterParamsKey) || undefined; const templatesByOrganizationIdQuery = useQuery( templatesByOrganizationId(organizationId), ); - const templatesQuery = useQuery(templates()); + const templatesQuery = useQuery(templates({ q: query })); const examplesQuery = useQuery({ ...templateExamples(organizationId), enabled: permissions.createTemplates, @@ -41,6 +45,7 @@ export const TemplatesPage: FC = () => { canCreateTemplates={permissions.createTemplates} examples={examplesQuery.data} templates={templatesQuery.data} + query={query} /> ) : ( Date: Thu, 11 Jul 2024 01:02:11 +0000 Subject: [PATCH 11/25] fix: fix format --- site/src/api/api.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 49e7e8254528f..535e984088ba9 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -187,7 +187,8 @@ export const watchBuildLogsByTemplateVersionId = ( const proto = location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket( - `${proto}//${location.host + `${proto}//${ + location.host }/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`, ); @@ -269,7 +270,8 @@ export const watchBuildLogsByBuildId = ( } const proto = location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket( - `${proto}//${location.host + `${proto}//${ + location.host }/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`, ); socket.binaryType = "blob"; @@ -380,7 +382,7 @@ export class MissingBuildParameters extends Error { * lexical scope. */ class ApiMethods { - constructor(protected readonly axios: AxiosInstance) { } + constructor(protected readonly axios: AxiosInstance) {} login = async ( email: string, From 369c59f0fe257046d9237915d649450382474f71 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 11 Jul 2024 02:28:56 +0000 Subject: [PATCH 12/25] fix: get activeOrg --- .../TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index 9af52896fcaf6..447e1758ec5be 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -1,6 +1,6 @@ import type { Interpolation, Theme } from "@emotion/react"; import type { FC } from "react"; -import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import type { Template, TemplateExample } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { @@ -85,7 +85,8 @@ export const TemplatesPageView: FC = ({ }) => { const navigate = useNavigate(); const isEmpty = templates && templates.length === 0; - const activeOrg = query?.split(":")[1] ?? "all"; + const q = query?.split(":") || []; + const activeOrg = q[0] === "organization" ? query?.split(":")[1] : "all"; const templatesByOrg = getTemplatesByOrg(templates ?? []); return ( From c41cdc4bf11241a3ce5bdf274d668e5fa74b4736 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 11 Jul 2024 03:02:48 +0000 Subject: [PATCH 13/25] fix: add format annotation --- codersdk/organizations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index d92b4e65f9883..9f6eafb24687a 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -367,7 +367,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui } type TemplateFilter struct { - OrganizationID uuid.UUID `json:"organization_id,omitempty" typescript:"-"` + OrganizationID uuid.UUID `json:"organization_id,omitempty" format:"uuid" typescript:"-"` FilterQuery string `json:"q,omitempty"` ExactName string } From 7f5d35ece910f48a3251cba71fcf7b2805805a7f Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 11 Jul 2024 03:25:37 +0000 Subject: [PATCH 14/25] chore: use organization display name --- .../TemplateCard/TemplateCard.stories.tsx | 2 +- .../templates/TemplateCard/TemplateCard.tsx | 2 +- .../TemplatesPageView.tsx | 27 +++++++++++-------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx b/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx index 7b55d2c0c8275..863b7a9a2bc0d 100644 --- a/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx +++ b/site/src/modules/templates/TemplateCard/TemplateCard.stories.tsx @@ -31,7 +31,7 @@ export const LongContentTemplate: Story = { template: { ...MockTemplate, display_name: "Very Long Template Name", - organization_name: "Very Long Organization Name", + organization_display_name: "Very Long Organization Name", description: "This is a very long test description. This is a very long test description. This is a very long test description. This is a very long test description", active_user_count: 999, diff --git a/site/src/modules/templates/TemplateCard/TemplateCard.tsx b/site/src/modules/templates/TemplateCard/TemplateCard.tsx index 25421d5a4a910..aa4a6bcf45c50 100644 --- a/site/src/modules/templates/TemplateCard/TemplateCard.tsx +++ b/site/src/modules/templates/TemplateCard/TemplateCard.tsx @@ -43,7 +43,7 @@ export const TemplateCard: FC = ({ ? template.display_name : template.name } - subtitle={template.organization_name} + subtitle={template.organization_display_name} avatar={ hasIcon && ( diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index 447e1758ec5be..e7726ce354a5e 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -56,21 +56,27 @@ export interface TemplatesPageViewProps { error?: unknown; } -export type TemplatesByOrg = Record; +export type TemplatesByOrg = Record< + string, + { displayName: string; count: number } +>; const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { const orgs: TemplatesByOrg = { - all: 0, + all: { displayName: "All Organizations", count: 0 }, }; templates.forEach((template) => { if (orgs[template.organization_name]) { - orgs[template.organization_name] += 1; + orgs[template.organization_name].count += 1; } else { - orgs[template.organization_name] = 1; + orgs[template.organization_name] = { + displayName: template.organization_display_name, + count: 1, + }; } - orgs.all += 1; + orgs.all.count += 1; }); return orgs; @@ -116,14 +122,13 @@ export const TemplatesPageView: FC = ({ ORGANIZATION - {Object.keys(templatesByOrg).map((org) => ( + {Object.entries(templatesByOrg).map(([key, value]) => ( - {org === "all" ? "All Organizations" : org} ( - {templatesByOrg[org] ?? 0}) + {value.displayName} ({value.count ?? 0}) ))} From 6e2a6d8482d1eaec9903d59f0eb3dd2e22ee6791 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 15 Jul 2024 02:18:01 +0000 Subject: [PATCH 15/25] feat: client side org filtering --- site/src/api/queries/templates.ts | 6 +- .../StarterTemplatesPage.tsx | 2 +- .../StarterTemplatesPageView.stories.tsx | 2 +- .../StarterTemplatesPageView.tsx | 2 +- .../TemplatesPageView.stories.tsx | 11 +-- .../TemplatesPageView.tsx | 90 +++++++++---------- .../src/pages/TemplatesPage/TemplatesPage.tsx | 17 ++-- ...terTemplates.ts => templateAggregators.ts} | 20 ++++- 8 files changed, 80 insertions(+), 70 deletions(-) rename site/src/utils/{starterTemplates.ts => templateAggregators.ts} (54%) diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 862eb86c1f24e..e582680ebc5f5 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -47,10 +47,10 @@ export const templatesByOrganizationId = ( }; }; -export const templates = (config: TemplateFilter = {}) => { +export const templates = (filter: TemplateFilter = {}) => { return { - queryKey: ["templates", config], - queryFn: () => API.getTemplates(config), + queryKey: ["templates", filter], + queryFn: () => API.getTemplates(filter), }; }; diff --git a/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx b/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx index d52c92a12df82..0e524e67749ff 100644 --- a/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx +++ b/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx @@ -5,7 +5,7 @@ import { templateExamples } from "api/queries/templates"; import type { TemplateExample } from "api/typesGenerated"; import { useDashboard } from "modules/dashboard/useDashboard"; import { pageTitle } from "utils/page"; -import { getTemplatesByTag } from "utils/starterTemplates"; +import { getTemplatesByTag } from "utils/templateAggregators"; import { StarterTemplatesPageView } from "./StarterTemplatesPageView"; const StarterTemplatesPage: FC = () => { diff --git a/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.stories.tsx b/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.stories.tsx index 228e8cae4ed9d..c2bb6a11f38b2 100644 --- a/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.stories.tsx +++ b/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.stories.tsx @@ -5,7 +5,7 @@ import { MockTemplateExample, MockTemplateExample2, } from "testHelpers/entities"; -import { getTemplatesByTag } from "utils/starterTemplates"; +import { getTemplatesByTag } from "utils/templateAggregators"; import { StarterTemplatesPageView } from "./StarterTemplatesPageView"; const meta: Meta = { diff --git a/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.tsx b/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.tsx index e0a6c4b975747..9d32a069cbf69 100644 --- a/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.tsx +++ b/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.tsx @@ -11,7 +11,7 @@ import { } from "components/PageHeader/PageHeader"; import { Stack } from "components/Stack/Stack"; import { TemplateExampleCard } from "modules/templates/TemplateExampleCard/TemplateExampleCard"; -import type { StarterTemplatesByTag } from "utils/starterTemplates"; +import type { StarterTemplatesByTag } from "utils/templateAggregators"; const getTagLabel = (tag: string) => { const labelByTag: Record = { diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx index ad9fede3cd611..63922cb9315e1 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx @@ -6,6 +6,7 @@ import { MockTemplateExample, MockTemplateExample2, } from "testHelpers/entities"; +import { getTemplatesByOrg } from "utils/templateAggregators"; import { TemplatesPageView } from "./TemplatesPageView"; const meta: Meta = { @@ -21,7 +22,7 @@ export const WithTemplates: Story = { args: { canCreateTemplates: true, error: undefined, - templates: [ + templatesByOrg: getTemplatesByOrg([ MockTemplate, { ...MockTemplate, @@ -62,7 +63,7 @@ export const WithTemplates: Story = { display_name: "Deprecated", description: "Template is incompatible", }, - ], + ]), examples: [], }, }; @@ -71,7 +72,7 @@ export const EmptyCanCreate: Story = { args: { canCreateTemplates: true, error: undefined, - templates: [], + templatesByOrg: getTemplatesByOrg([]), examples: [MockTemplateExample, MockTemplateExample2], }, }; @@ -79,7 +80,7 @@ export const EmptyCanCreate: Story = { export const EmptyCannotCreate: Story = { args: { error: undefined, - templates: [], + templatesByOrg: getTemplatesByOrg([]), examples: [MockTemplateExample, MockTemplateExample2], canCreateTemplates: false, }, @@ -90,7 +91,7 @@ export const Error: Story = { error: mockApiError({ message: "Something went wrong fetching templates.", }), - templates: undefined, + templatesByOrg: undefined, examples: undefined, canCreateTemplates: false, }, diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index e7726ce354a5e..888e75e9e82d8 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -1,7 +1,7 @@ import type { Interpolation, Theme } from "@emotion/react"; import type { FC } from "react"; -import { Link, useNavigate } from "react-router-dom"; -import type { Template, TemplateExample } from "api/typesGenerated"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import type { TemplateExample } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { HelpTooltip, @@ -12,6 +12,7 @@ import { HelpTooltipTitle, HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; +import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; import { PageHeader, @@ -21,6 +22,7 @@ import { import { Stack } from "components/Stack/Stack"; import { TemplateCard } from "modules/templates/TemplateCard/TemplateCard"; import { docs } from "utils/docs"; +import type { TemplatesByOrg } from "utils/templateAggregators"; import { CreateTemplateButton } from "../CreateTemplateButton"; import { EmptyTemplates } from "../EmptyTemplates"; @@ -49,51 +51,32 @@ const TemplateHelpTooltip: FC = () => { }; export interface TemplatesPageViewProps { - templates: Template[] | undefined; + templatesByOrg?: TemplatesByOrg; examples: TemplateExample[] | undefined; canCreateTemplates: boolean; - query: string | undefined; error?: unknown; } -export type TemplatesByOrg = Record< - string, - { displayName: string; count: number } ->; - -const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { - const orgs: TemplatesByOrg = { - all: { displayName: "All Organizations", count: 0 }, - }; - - templates.forEach((template) => { - if (orgs[template.organization_name]) { - orgs[template.organization_name].count += 1; - } else { - orgs[template.organization_name] = { - displayName: template.organization_display_name, - count: 1, - }; - } - - orgs.all.count += 1; - }); - - return orgs; +const sortOrgs = (templatesByOrg: TemplatesByOrg) => { + return templatesByOrg + ? Object.keys(templatesByOrg).sort((a, b) => a.localeCompare(b)) + : undefined; }; export const TemplatesPageView: FC = ({ - templates, + templatesByOrg, examples, canCreateTemplates, - query, error, }) => { const navigate = useNavigate(); - const isEmpty = templates && templates.length === 0; - const q = query?.split(":") || []; - const activeOrg = q[0] === "organization" ? query?.split(":")[1] : "all"; - const templatesByOrg = getTemplatesByOrg(templates ?? []); + const [urlParams] = useSearchParams(); + const isEmpty = templatesByOrg && templatesByOrg["all"].length === 0; + const orgs = templatesByOrg ? sortOrgs(templatesByOrg) : undefined; + const activeOrg = urlParams.get("org") ?? "all"; + const visibleTemplates = templatesByOrg + ? templatesByOrg[activeOrg] + : undefined; return ( @@ -108,7 +91,7 @@ export const TemplatesPageView: FC = ({ - {templates && templates.length > 0 && ( + {!isEmpty && ( Select a template to create a workspace. @@ -119,19 +102,28 @@ export const TemplatesPageView: FC = ({ )} + {Boolean(!templatesByOrg) && } + - - ORGANIZATION - {Object.entries(templatesByOrg).map(([key, value]) => ( - - {value.displayName} ({value.count ?? 0}) - - ))} - + {templatesByOrg && orgs && ( + + ORGANIZATION + {orgs.map((org) => ( + + {org} ({templatesByOrg[org].length}) + + ))} + + )}
= ({ examples={examples ?? []} /> ) : ( - templates && - templates.map((template) => ( + visibleTemplates && + visibleTemplates.map((template) => ( ({ backgroundColor: theme.palette.background.paper, diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index 49f0dea1197c8..d8b60562f7d0d 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -1,7 +1,6 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; -import { useSearchParams } from "react-router-dom"; import { templateExamples, templatesByOrganizationId, @@ -9,21 +8,22 @@ import { } from "api/queries/templates"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { filterParamsKey } from "utils/filters"; import { pageTitle } from "utils/page"; +import { getTemplatesByOrg } from "utils/templateAggregators"; import { TemplatesPageView as MultiOrgTemplatesPageView } from "./MultiOrgTemplatePage/TemplatesPageView"; import { TemplatesPageView } from "./TemplatePage/TemplatesPageView"; export const TemplatesPage: FC = () => { const { permissions } = useAuthenticated(); const { organizationId, experiments } = useDashboard(); - const [searchParams] = useSearchParams(); - const query = searchParams.get(filterParamsKey) || undefined; const templatesByOrganizationIdQuery = useQuery( templatesByOrganizationId(organizationId), ); - const templatesQuery = useQuery(templates({ q: query })); + const templatesQuery = useQuery(templates()); + const templatesByOrg = templatesQuery.data + ? getTemplatesByOrg(templatesQuery.data) + : undefined; const examplesQuery = useQuery({ ...templateExamples(organizationId), enabled: permissions.createTemplates, @@ -41,11 +41,10 @@ export const TemplatesPage: FC = () => { {multiOrgExperimentEnabled ? ( ) : ( ; +export type TemplatesByOrg = Record; export const getTemplatesByTag = ( templates: TemplateExample[], @@ -22,3 +23,20 @@ export const getTemplatesByTag = ( return tags; }; + +export const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { + const orgs: TemplatesByOrg = { + all: templates, + }; + + templates.forEach((template) => { + const org = template.organization_name; + if (orgs[org]) { + orgs[org].push(template); + } else { + orgs[org] = [template]; + } + }); + + return orgs; +}; From 978c047176525f379a0f6a4d4cab4bb7fa0fc4d8 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 15 Jul 2024 02:38:08 +0000 Subject: [PATCH 16/25] fix: use org display name --- .../TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index 888e75e9e82d8..88a57ddb21648 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -119,7 +119,10 @@ export const TemplatesPageView: FC = ({ org === activeOrg && styles.tagLinkActive, ]} > - {org} ({templatesByOrg[org].length}) + {org === "all" + ? org + : templatesByOrg[org][0].organization_display_name} + ({templatesByOrg[org].length}) ))} From aaed038b5eba9c5a5a33660083e0315d3dfc16aa Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 15 Jul 2024 16:32:19 +0000 Subject: [PATCH 17/25] fix: add ExactName --- site/src/api/queries/templates.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index e582680ebc5f5..d6f6b5bd7b88d 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -47,7 +47,9 @@ export const templatesByOrganizationId = ( }; }; -export const templates = (filter: TemplateFilter = {}) => { +export const templates = (filter: TemplateFilter = { + ExactName: "" +}) => { return { queryKey: ["templates", filter], queryFn: () => API.getTemplates(filter), From 8d84ad96cf396a9bbbc1be900b04aafd38cd9dd5 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 15 Jul 2024 16:32:51 +0000 Subject: [PATCH 18/25] feat: show orgs filter only if more than 1 org --- .../TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index 88a57ddb21648..4fd1796248212 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -105,7 +105,7 @@ export const TemplatesPageView: FC = ({ {Boolean(!templatesByOrg) && } - {templatesByOrg && orgs && ( + {templatesByOrg && orgs && orgs.length > 2 && ( From a1c6169a43ac2d989dd3b4f9cc337713d604a9c9 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 16 Jul 2024 14:56:05 +0000 Subject: [PATCH 19/25] chore: updates for PR review --- codersdk/organizations.go | 2 +- site/src/api/queries/templates.ts | 4 +--- site/src/api/typesGenerated.ts | 2 +- site/src/utils/templateAggregators.ts | 12 ++++++------ 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 9f6eafb24687a..4b86c449e0ea4 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -369,7 +369,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui type TemplateFilter struct { OrganizationID uuid.UUID `json:"organization_id,omitempty" format:"uuid" typescript:"-"` FilterQuery string `json:"q,omitempty"` - ExactName string + ExactName string `json:"exact_name,omitempty"` } // asRequestOption returns a function that can be used in (*Client).Request. diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index d6f6b5bd7b88d..312e6269498bc 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -47,9 +47,7 @@ export const templatesByOrganizationId = ( }; }; -export const templates = (filter: TemplateFilter = { - ExactName: "" -}) => { +export const templates = (filter?: TemplateFilter) => { return { queryKey: ["templates", filter], queryFn: () => API.getTemplates(filter), diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 2b7e32787b2cd..38d791aa94a63 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1224,8 +1224,8 @@ export interface TemplateExample { // From codersdk/organizations.go export interface TemplateFilter { - readonly ExactName: string; readonly q?: string; + readonly ExactName: string; } // From codersdk/templates.go diff --git a/site/src/utils/templateAggregators.ts b/site/src/utils/templateAggregators.ts index d31ac1a866ebb..ecd0d86c18a51 100644 --- a/site/src/utils/templateAggregators.ts +++ b/site/src/utils/templateAggregators.ts @@ -10,16 +10,16 @@ export const getTemplatesByTag = ( all: templates, }; - templates.forEach((template) => { - template.tags.forEach((tag) => { + for (const template of templates) { + for (const tag of template.tags) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- this can be undefined if (tags[tag]) { tags[tag].push(template); } else { tags[tag] = [template]; } - }); - }); + }; + }; return tags; }; @@ -29,14 +29,14 @@ export const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { all: templates, }; - templates.forEach((template) => { + for (const template of templates) { const org = template.organization_name; if (orgs[org]) { orgs[org].push(template); } else { orgs[org] = [template]; } - }); + }; return orgs; }; From 15542c053483f02c83b18dac041aeab028cd2336 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 16 Jul 2024 17:18:19 +0000 Subject: [PATCH 20/25] fix: fix format --- site/src/utils/templateAggregators.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/utils/templateAggregators.ts b/site/src/utils/templateAggregators.ts index ecd0d86c18a51..3960c6b8b688c 100644 --- a/site/src/utils/templateAggregators.ts +++ b/site/src/utils/templateAggregators.ts @@ -18,8 +18,8 @@ export const getTemplatesByTag = ( } else { tags[tag] = [template]; } - }; - }; + } + } return tags; }; @@ -36,7 +36,7 @@ export const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { } else { orgs[org] = [template]; } - }; + } return orgs; }; From 8f4c56fb9cd6b713e96e70615175bc445fd9f956 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 16 Jul 2024 17:32:44 +0000 Subject: [PATCH 21/25] chore: add story for multi org --- .../TemplatesPageView.stories.tsx | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx index 63922cb9315e1..10eacf0ae6f85 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.stories.tsx @@ -18,7 +18,7 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const WithTemplates: Story = { +export const WithTemplatesSingleOrgs: Story = { args: { canCreateTemplates: true, error: undefined, @@ -68,6 +68,62 @@ export const WithTemplates: Story = { }, }; +export const WithTemplatesMultipleOrgs: Story = { + args: { + canCreateTemplates: true, + error: undefined, + templatesByOrg: getTemplatesByOrg([ + MockTemplate, + { + ...MockTemplate, + organization_id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8a1", + organization_name: "first-org", + organization_display_name: "First Org", + active_user_count: -1, + description: "🚀 Some new template that has no activity data", + icon: "/icon/goland.svg", + }, + { + ...MockTemplate, + organization_id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8a1", + organization_name: "first-org", + organization_display_name: "First Org", + 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. ", + }, + { + ...MockTemplate, + name: "template-without-icon", + display_name: "No Icon", + description: "This one has no icon", + icon: "", + }, + { + ...MockTemplate, + name: "template-without-icon-deprecated", + display_name: "Deprecated No Icon", + description: "This one has no icon and is deprecated", + deprecated: true, + deprecation_message: "This template is so old, it's deprecated", + icon: "", + }, + { + ...MockTemplate, + name: "deprecated-template", + display_name: "Deprecated", + description: "Template is incompatible", + }, + ]), + examples: [], + }, +}; + export const EmptyCanCreate: Story = { args: { canCreateTemplates: true, From a282baceb3ed88fc711a1d5d76014d3a3a3cf15f Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 17 Jul 2024 01:59:17 +0000 Subject: [PATCH 22/25] fix: aggregate templates by organization id --- .../TemplatesPageView.tsx | 21 ++++++------------- site/src/utils/templateAggregators.ts | 12 ++++++----- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index 4fd1796248212..6d31bd62127c3 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -57,12 +57,6 @@ export interface TemplatesPageViewProps { error?: unknown; } -const sortOrgs = (templatesByOrg: TemplatesByOrg) => { - return templatesByOrg - ? Object.keys(templatesByOrg).sort((a, b) => a.localeCompare(b)) - : undefined; -}; - export const TemplatesPageView: FC = ({ templatesByOrg, examples, @@ -72,7 +66,6 @@ export const TemplatesPageView: FC = ({ const navigate = useNavigate(); const [urlParams] = useSearchParams(); const isEmpty = templatesByOrg && templatesByOrg["all"].length === 0; - const orgs = templatesByOrg ? sortOrgs(templatesByOrg) : undefined; const activeOrg = urlParams.get("org") ?? "all"; const visibleTemplates = templatesByOrg ? templatesByOrg[activeOrg] @@ -110,19 +103,17 @@ export const TemplatesPageView: FC = ({ css={{ width: 208, flexShrink: 0, position: "sticky", top: 48 }} > ORGANIZATION - {orgs.map((org) => ( + {Object.entries(templatesByOrg).map((org) => ( - {org === "all" - ? org - : templatesByOrg[org][0].organization_display_name} - ({templatesByOrg[org].length}) + {org[0] === "all" ? "all" : org[1][0].organization_display_name}{" "} + ({org[1].length}) ))} diff --git a/site/src/utils/templateAggregators.ts b/site/src/utils/templateAggregators.ts index 3960c6b8b688c..4510b33eb579b 100644 --- a/site/src/utils/templateAggregators.ts +++ b/site/src/utils/templateAggregators.ts @@ -25,12 +25,10 @@ export const getTemplatesByTag = ( }; export const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { - const orgs: TemplatesByOrg = { - all: templates, - }; + const orgs: TemplatesByOrg = {}; for (const template of templates) { - const org = template.organization_name; + const org = template.organization_id; if (orgs[org]) { orgs[org].push(template); } else { @@ -38,5 +36,9 @@ export const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { } } - return orgs; + const sortedOrgs = Object.fromEntries( + Object.entries(orgs).sort(([, a], [, b]) => a[0].organization_name.localeCompare(b[0].organization_name)) + ); + + return { all: templates, ...sortedOrgs }; }; From b092644c502a07b4c10329a0e47e86923e60dfc7 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 17 Jul 2024 02:00:12 +0000 Subject: [PATCH 23/25] fix: fix format --- site/src/utils/templateAggregators.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/utils/templateAggregators.ts b/site/src/utils/templateAggregators.ts index 4510b33eb579b..93f368263b79b 100644 --- a/site/src/utils/templateAggregators.ts +++ b/site/src/utils/templateAggregators.ts @@ -37,7 +37,9 @@ export const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => { } const sortedOrgs = Object.fromEntries( - Object.entries(orgs).sort(([, a], [, b]) => a[0].organization_name.localeCompare(b[0].organization_name)) + Object.entries(orgs).sort(([, a], [, b]) => + a[0].organization_name.localeCompare(b[0].organization_name), + ), ); return { all: templates, ...sortedOrgs }; From 32376e64616e82c6a90076ed8807c2f5d7689c67 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 17 Jul 2024 02:03:01 +0000 Subject: [PATCH 24/25] fix: check org count --- .../TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx index 6d31bd62127c3..095930fa16c94 100644 --- a/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/MultiOrgTemplatePage/TemplatesPageView.tsx @@ -98,7 +98,7 @@ export const TemplatesPageView: FC = ({ {Boolean(!templatesByOrg) && } - {templatesByOrg && orgs && orgs.length > 2 && ( + {templatesByOrg && Object.keys(templatesByOrg).length > 2 && ( From 801138a25255c3155223e7b9678e0e80ce2d2ff7 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 17 Jul 2024 14:50:58 +0000 Subject: [PATCH 25/25] fix: update ExactName type --- codersdk/organizations.go | 2 +- site/src/api/typesGenerated.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 4b86c449e0ea4..998c891aff564 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -369,7 +369,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui type TemplateFilter struct { OrganizationID uuid.UUID `json:"organization_id,omitempty" format:"uuid" typescript:"-"` FilterQuery string `json:"q,omitempty"` - ExactName string `json:"exact_name,omitempty"` + ExactName string `json:"exact_name,omitempty" typescript:"-"` } // asRequestOption returns a function that can be used in (*Client).Request. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 38d791aa94a63..944e5bbc37f9e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1225,7 +1225,6 @@ export interface TemplateExample { // From codersdk/organizations.go export interface TemplateFilter { readonly q?: string; - readonly ExactName: string; } // From codersdk/templates.go