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

Skip to content

feat: create multi-org template creation page #13879

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d640312
feat: initial changes for multi org template creation
jaaydenh Jul 12, 2024
1c973ae
feat: styling and add template creation options
jaaydenh Jul 18, 2024
6b5e7ec
chore: remove unused imports
jaaydenh Jul 18, 2024
fbd6ca3
fix: use theme on cardTitle
jaaydenh Jul 18, 2024
013522b
fix: remove unused import
jaaydenh Jul 18, 2024
f547bfc
fix: pass key directly to JSX without using spread
jaaydenh Jul 19, 2024
6b6c635
fix: use correct path
jaaydenh Jul 19, 2024
b4e3c67
fix: fix test
jaaydenh Jul 19, 2024
2636753
chore: handle merge changes
jaaydenh Jul 19, 2024
c113979
fix: remove CreateTemplateButton
jaaydenh Jul 19, 2024
cb53396
fix: update template examples for multi-org
jaaydenh Jul 23, 2024
789699a
feat: pass along organizationId when creating templates
jaaydenh Jul 23, 2024
8b03b11
fix: improve organization autocomplete
jaaydenh Jul 24, 2024
b3aa04f
Feat: move org dropdown and add orgId search param
jaaydenh Jul 24, 2024
03768c4
chore: show org autocomplete if experiment is enabled
jaaydenh Jul 24, 2024
7baba1e
fix: remove unnecessary query param
jaaydenh Jul 24, 2024
f513633
fix: cleanup
jaaydenh Jul 24, 2024
b350b95
fix: fix tests
jaaydenh Jul 24, 2024
2810cd3
fix: fix create template form stories
jaaydenh Jul 24, 2024
6499543
fix
jaaydenh Jul 24, 2024
c52c3dc
fix: merge issues
jaaydenh Jul 25, 2024
6205129
chore: use default org for example templates
jaaydenh Jul 26, 2024
b721a43
feat: add organizationId to the templates route
jaaydenh Jul 26, 2024
41a414e
fix: add missing useMemo dependency
jaaydenh Jul 26, 2024
e23475b
fix: add organizationId dependency
jaaydenh Jul 26, 2024
53d4a3e
feat: update tests for organization in templates route
jaaydenh Jul 26, 2024
7d92893
feat: gate org dropdown to multiple_organizations feature
jaaydenh Jul 26, 2024
73ae86b
chore: set default for organization route param
jaaydenh Jul 26, 2024
9bb1139
feat: update template routes for organizations
jaaydenh Jul 26, 2024
1375b30
feat: add templates route
jaaydenh Jul 26, 2024
aa452fe
feat: use templates_route
jaaydenh Jul 26, 2024
0830dcc
Merge branch 'main' into multi-org-create-template
aslilac Jul 26, 2024
b637620
the whirly durly
aslilac Jul 26, 2024
0fc9acc
Merge branch 'main' into multi-org-create-template
aslilac Jul 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion site/src/api/queries/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ import type {
import { delay } from "utils/delay";
import { getTemplateVersionFiles } from "utils/templateVersion";

export const templateKey = (templateId: string) => ["template", templateId];

export const template = (templateId: string): QueryOptions<Template> => {
return {
queryKey: templateKey(templateId),
queryFn: async () => API.getTemplate(templateId),
};
};

export const templateByNameKey = (organizationId: string, name: string) => [
organizationId,
"template",
name,
"settings",
];

export const templateByName = (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { css } from "@emotion/css";
import Autocomplete from "@mui/material/Autocomplete";
import CircularProgress from "@mui/material/CircularProgress";
import TextField from "@mui/material/TextField";
import {
type ChangeEvent,
type ComponentProps,
type FC,
useState,
} from "react";
import { useQuery } from "react-query";
import { organizations } from "api/queries/organizations";
import type { Organization } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/AvatarData/AvatarData";
import { useDebouncedFunction } from "hooks/debounce";

export type OrganizationAutocompleteProps = {
value: Organization | null;
onChange: (organization: Organization | null) => void;
label?: string;
className?: string;
size?: ComponentProps<typeof TextField>["size"];
required?: boolean;
};

export const OrganizationAutocomplete: FC<OrganizationAutocompleteProps> = ({
value,
onChange,
label,
className,
size = "small",
required,
}) => {
const [autoComplete, setAutoComplete] = useState<{
value: string;
open: boolean;
}>({
value: value?.name ?? "",
open: false,
});
const organizationsQuery = useQuery(organizations());

const { debounced: debouncedInputOnChange } = useDebouncedFunction(
(event: ChangeEvent<HTMLInputElement>) => {
setAutoComplete((state) => ({
...state,
value: event.target.value,
}));
},
750,
);

return (
<Autocomplete
noOptionsText="No organizations found"
className={className}
options={organizationsQuery.data ?? []}
loading={organizationsQuery.isLoading}
value={value}
data-testid="organization-autocomplete"
open={autoComplete.open}
isOptionEqualToValue={(a, b) => a.name === b.name}
getOptionLabel={(option) => option.display_name}
onOpen={() => {
setAutoComplete((state) => ({
...state,
open: true,
}));
}}
onClose={() => {
setAutoComplete({
value: value?.name ?? "",
open: false,
});
}}
onChange={(_, newValue) => {
onChange(newValue);
}}
renderOption={({ key, ...props }, option) => (
<li key={key} {...props}>
<AvatarData
title={option.display_name}
subtitle={option.name}
src={option.icon}
/>
</li>
)}
renderInput={(params) => (
<TextField
{...params}
required={required}
fullWidth
size={size}
label={label}
autoFocus
placeholder="Organization name"
css={{
"&:not(:has(label))": {
margin: 0,
},
}}
InputProps={{
...params.InputProps,
onChange: debouncedInputOnChange,
startAdornment: value && (
<Avatar size="sm" src={value.icon}>
{value.name}
</Avatar>
),
endAdornment: (
<>
{organizationsQuery.isFetching && autoComplete.open && (
<CircularProgress size={16} />
)}
{params.InputProps.endAdornment}
</>
),
classes: { root },
}}
InputLabelProps={{
shrink: true,
}}
/>
)}
/>
);
};

const root = css`
padding-left: 14px !important; // Same padding left as input
gap: 4px;
`;
22 changes: 10 additions & 12 deletions site/src/components/UserAutocomplete/UserAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type UserAutocompleteProps = {
label?: string;
className?: string;
size?: ComponentProps<typeof TextField>["size"];
required?: boolean;
};

export const UserAutocomplete: FC<UserAutocompleteProps> = ({
Expand All @@ -30,6 +31,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
label,
className,
size = "small",
required,
}) => {
const [autoComplete, setAutoComplete] = useState<{
value: string;
Expand Down Expand Up @@ -59,16 +61,15 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({

return (
<Autocomplete
// Since the values are filtered by the API we don't need to filter them
// in the FE because it can causes some mismatches.
filterOptions={(user) => user}
noOptionsText="No users found"
className={className}
options={usersQuery.data?.users ?? []}
loading={usersQuery.isLoading}
value={value}
id="user-autocomplete"
data-testid="user-autocomplete"
open={autoComplete.open}
isOptionEqualToValue={(a, b) => a.username === b.username}
getOptionLabel={(option) => option.email}
onOpen={() => {
setAutoComplete((state) => ({
...state,
Expand All @@ -84,12 +85,8 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
onChange={(_, newValue) => {
onChange(newValue);
}}
isOptionEqualToValue={(option: User, value: User) =>
option.username === value.username
}
getOptionLabel={(option) => option.email}
renderOption={(props, option) => (
<li {...props}>
renderOption={({ key, ...props }, option) => (
<li key={key} {...props}>
<AvatarData
title={option.username}
subtitle={option.email}
Expand All @@ -100,6 +97,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
renderInput={(params) => (
<TextField
{...params}
required={required}
fullWidth
size={size}
label={label}
Expand All @@ -119,9 +117,9 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
),
endAdornment: (
<>
{usersQuery.isFetching && autoComplete.open ? (
{usersQuery.isFetching && autoComplete.open && (
<CircularProgress size={16} />
) : null}
)}
{params.InputProps.endAdornment}
</>
),
Expand Down
17 changes: 17 additions & 0 deletions site/src/modules/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* @fileoverview TODO: centralize navigation code here! URL constants, URL formatting, all of it
*/
import type { Experiments } from "api/typesGenerated";

export function withFilter(path: string, filter: string) {
return path + (filter ? `?filter=${encodeURIComponent(filter)}` : "");
Expand All @@ -9,3 +10,19 @@ export function withFilter(path: string, filter: string) {
export const AUDIT_LINK = "/audit";

export const USERS_LINK = withFilter("/users", "status:active");

export const TEMPLATES_ROUTE = (
organizationId: string,
templateName: string,
routeSuffix: string = "",
orgsEnabled: boolean = false,
experiments: Experiments = [],
) => {
const multiOrgExperimentEnabled = experiments.includes("multi-organization");

if (multiOrgExperimentEnabled && orgsEnabled) {
return `/templates/${organizationId}/${templateName}${routeSuffix}`;
}

return `/templates/${templateName}${routeSuffix}`;
};
13 changes: 8 additions & 5 deletions site/src/modules/templates/TemplateFiles/TemplateFiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@ import type { TemplateVersionFiles } from "utils/templateVersion";
import { TemplateFileTree } from "./TemplateFileTree";

interface TemplateFilesProps {
organizationName: string;
templateName: string;
versionName: string;
currentFiles: TemplateVersionFiles;
/**
* Files used to compare with current files
*/
baseFiles?: TemplateVersionFiles;
versionName: string;
templateName: string;
}

export const TemplateFiles: FC<TemplateFilesProps> = ({
organizationName,
templateName,
versionName,
currentFiles,
baseFiles,
versionName,
templateName,
}) => {
const filenames = Object.keys(currentFiles);
const theme = useTheme();
Expand Down Expand Up @@ -104,7 +106,8 @@ export const TemplateFiles: FC<TemplateFilesProps> = ({

<div css={{ marginLeft: "auto" }}>
<Link
to={`/templates/${templateName}/versions/${versionName}/edit?path=${filename}`}
// TODO: skip org name if we're not licensed
to={`/templates/${organizationName}/${templateName}/versions/${versionName}/edit?path=${filename}`}
css={{
display: "flex",
gap: 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export const Language = {
};

interface TooltipProps {
onUpdateVersion: () => void;
organizationName: string;
templateName: string;
latestVersionId: string;
onUpdateVersion: () => void;
ariaLabel?: string;
}

Expand All @@ -48,10 +49,11 @@ export const WorkspaceOutdatedTooltip: FC<TooltipProps> = (props) => {
};

export const WorkspaceOutdatedTooltipContent: FC<TooltipProps> = ({
organizationName,
templateName,
latestVersionId,
onUpdateVersion,
ariaLabel,
latestVersionId,
templateName,
}) => {
const popover = usePopover();
const { data: activeVersion } = useQuery({
Expand All @@ -71,7 +73,8 @@ export const WorkspaceOutdatedTooltipContent: FC<TooltipProps> = ({
<div>
{activeVersion ? (
<Link
href={`/templates/${templateName}/versions/${activeVersion.name}`}
// TODO: skip org name if we're not licensed
href={`/templates/${organizationName}/${templateName}/versions/${activeVersion.name}`}
target="_blank"
css={{ color: theme.palette.primary.light }}
>
Expand Down
Loading
Loading