Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit554c4ab

Browse files
authored
feat: implement multi-org template gallery (#13784)
* feat: initial changes for multi-org templates page* feat: add TemplateCard component* feat: add component stories* chore: update template query naming* fix: fix formatting* feat: template card interaction and navigation* fix: copy updates* chore: update TemplateFilter type to include FilterQuery* chore: update typesGenerated.ts* feat: update template filter api logic* fix: fix format* fix: get activeOrg* fix: add format annotation* chore: use organization display name* feat: client side org filtering* fix: use org display name* fix: add ExactName* feat: show orgs filter only if more than 1 org* chore: updates for PR review* fix: fix format* chore: add story for multi org* fix: aggregate templates by organization id* fix: fix format* fix: check org count* fix: update ExactName type
1 parent40609c2 commit554c4ab

21 files changed

+663
-64
lines changed

‎codersdk/organizations.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,9 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui
400400
}
401401

402402
typeTemplateFilterstruct {
403-
OrganizationID uuid.UUID
404-
ExactNamestring
403+
OrganizationID uuid.UUID`json:"organization_id,omitempty" format:"uuid" typescript:"-"`
404+
FilterQuerystring`json:"q,omitempty"`
405+
ExactNamestring`json:"exact_name,omitempty" typescript:"-"`
405406
}
406407

407408
// asRequestOption returns a function that can be used in (*Client).Request.
@@ -419,6 +420,11 @@ func (f TemplateFilter) asRequestOption() RequestOption {
419420
params=append(params,fmt.Sprintf("exact_name:%q",f.ExactName))
420421
}
421422

423+
iff.FilterQuery!="" {
424+
// If custom stuff is added, just add it on here.
425+
params=append(params,f.FilterQuery)
426+
}
427+
422428
q:=r.URL.Query()
423429
q.Set("q",strings.Join(params," "))
424430
r.URL.RawQuery=q.Encode()

‎site/src/api/api.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ class ApiMethods {
578578
returnresponse.data;
579579
};
580580

581-
getTemplates=async(
581+
getTemplatesByOrganizationId=async(
582582
organizationId:string,
583583
options?:TemplateOptions,
584584
):Promise<TypesGen.Template[]>=>{
@@ -598,6 +598,14 @@ class ApiMethods {
598598
returnresponse.data;
599599
};
600600

601+
getTemplates=async(
602+
options?:TypesGen.TemplateFilter,
603+
):Promise<TypesGen.Template[]>=>{
604+
consturl=getURLWithSearchParams("/api/v2/templates",options);
605+
constresponse=awaitthis.axios.get<TypesGen.Template[]>(url);
606+
returnresponse.data;
607+
};
608+
601609
getTemplateByName=async(
602610
organizationId:string,
603611
name:string,

‎site/src/api/queries/audits.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import{API}from"api/api";
22
importtype{AuditLogResponse}from"api/typesGenerated";
3-
import{useFilterParamsKey}from"components/Filter/filter";
43
importtype{UsePaginatedQueryOptions}from"hooks/usePaginatedQuery";
4+
import{filterParamsKey}from"utils/filters";
55

66
exportfunctionpaginatedAudits(
77
searchParams:URLSearchParams,
88
):UsePaginatedQueryOptions<AuditLogResponse,string>{
99
return{
1010
searchParams,
11-
queryPayload:()=>searchParams.get(useFilterParamsKey)??"",
11+
queryPayload:()=>searchParams.get(filterParamsKey)??"",
1212
queryKey:({ payload, pageNumber})=>{
1313
return["auditLogs",payload,pageNumber]asconst;
1414
},

‎site/src/api/queries/templates.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
importtype{MutationOptions,QueryClient,QueryOptions}from"react-query";
22
import{API}from"api/api";
33
importtype{
4+
TemplateFilter,
45
CreateTemplateRequest,
56
CreateTemplateVersionRequest,
67
ProvisionerJob,
@@ -30,16 +31,26 @@ export const templateByName = (
3031
};
3132
};
3233

33-
constgetTemplatesQueryKey=(organizationId:string,deprecated?:boolean)=>[
34-
organizationId,
35-
"templates",
36-
deprecated,
37-
];
34+
constgetTemplatesByOrganizationIdQueryKey=(
35+
organizationId:string,
36+
deprecated?:boolean,
37+
)=>[organizationId,"templates",deprecated];
38+
39+
exportconsttemplatesByOrganizationId=(
40+
organizationId:string,
41+
deprecated?:boolean,
42+
)=>{
43+
return{
44+
queryKey:getTemplatesByOrganizationIdQueryKey(organizationId,deprecated),
45+
queryFn:()=>
46+
API.getTemplatesByOrganizationId(organizationId,{ deprecated}),
47+
};
48+
};
3849

39-
exportconsttemplates=(organizationId:string,deprecated?:boolean)=>{
50+
exportconsttemplates=(filter?:TemplateFilter)=>{
4051
return{
41-
queryKey:getTemplatesQueryKey(organizationId,deprecated),
42-
queryFn:()=>API.getTemplates(organizationId,{ deprecated}),
52+
queryKey:["templates",filter],
53+
queryFn:()=>API.getTemplates(filter),
4354
};
4455
};
4556

@@ -92,7 +103,10 @@ export const setGroupRole = (
92103

93104
exportconsttemplateExamples=(organizationId:string)=>{
94105
return{
95-
queryKey:[...getTemplatesQueryKey(organizationId),"examples"],
106+
queryKey:[
107+
...getTemplatesByOrganizationIdQueryKey(organizationId),
108+
"examples",
109+
],
96110
queryFn:()=>API.getTemplateExamples(organizationId),
97111
};
98112
};

‎site/src/api/typesGenerated.ts

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎site/src/components/Filter/filter.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import{InputGroup}from"components/InputGroup/InputGroup";
1717
import{SearchField}from"components/SearchField/SearchField";
1818
import{useDebouncedFunction}from"hooks/debounce";
19+
import{filterParamsKey}from"utils/filters";
1920

2021
exporttypePresetFilter={
2122
name:string;
@@ -35,21 +36,19 @@ type UseFilterConfig = {
3536
onUpdate?:(newValue:string)=>void;
3637
};
3738

38-
exportconstuseFilterParamsKey="filter";
39-
4039
exportconstuseFilter=({
4140
fallbackFilter="",
4241
searchParamsResult,
4342
onUpdate,
4443
}:UseFilterConfig)=>{
4544
const[searchParams,setSearchParams]=searchParamsResult;
46-
constquery=searchParams.get(useFilterParamsKey)??fallbackFilter;
45+
constquery=searchParams.get(filterParamsKey)??fallbackFilter;
4746

4847
constupdate=(newValues:string|FilterValues)=>{
4948
constserialized=
5049
typeofnewValues==="string" ?newValues :stringifyFilter(newValues);
5150

52-
searchParams.set(useFilterParamsKey,serialized);
51+
searchParams.set(filterParamsKey,serialized);
5352
setSearchParams(searchParams);
5453

5554
if(onUpdate!==undefined){
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
importtype{Meta,StoryObj}from"@storybook/react";
2+
import{chromatic}from"testHelpers/chromatic";
3+
import{MockTemplate}from"testHelpers/entities";
4+
import{TemplateCard}from"./TemplateCard";
5+
6+
constmeta:Meta<typeofTemplateCard>={
7+
title:"modules/templates/TemplateCard",
8+
parameters:{ chromatic},
9+
component:TemplateCard,
10+
args:{
11+
template:MockTemplate,
12+
},
13+
};
14+
15+
exportdefaultmeta;
16+
typeStory=StoryObj<typeofTemplateCard>;
17+
18+
exportconstTemplate:Story={};
19+
20+
exportconstDeprecatedTemplate:Story={
21+
args:{
22+
template:{
23+
...MockTemplate,
24+
deprecated:true,
25+
},
26+
},
27+
};
28+
29+
exportconstLongContentTemplate:Story={
30+
args:{
31+
template:{
32+
...MockTemplate,
33+
display_name:"Very Long Template Name",
34+
organization_display_name:"Very Long Organization Name",
35+
description:
36+
"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",
37+
active_user_count:999,
38+
},
39+
},
40+
};
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
importtype{Interpolation,Theme}from"@emotion/react";
2+
importArrowForwardOutlinedfrom"@mui/icons-material/ArrowForwardOutlined";
3+
importButtonfrom"@mui/material/Button";
4+
importtype{FC,HTMLAttributes}from"react";
5+
import{LinkasRouterLink,useNavigate}from"react-router-dom";
6+
importtype{Template}from"api/typesGenerated";
7+
import{ExternalAvatar}from"components/Avatar/Avatar";
8+
import{AvatarData}from"components/AvatarData/AvatarData";
9+
import{DeprecatedBadge}from"components/Badges/Badges";
10+
11+
typeTemplateCardProps=HTMLAttributes<HTMLDivElement>&{
12+
template:Template;
13+
};
14+
15+
exportconstTemplateCard:FC<TemplateCardProps>=({
16+
template,
17+
...divProps
18+
})=>{
19+
constnavigate=useNavigate();
20+
consttemplatePageLink=`/templates/${template.name}`;
21+
consthasIcon=template.icon&&template.icon!=="";
22+
23+
consthandleKeyDown=(e:React.KeyboardEvent)=>{
24+
if(e.key==="Enter"&&e.currentTarget===e.target){
25+
navigate(templatePageLink);
26+
}
27+
};
28+
29+
return(
30+
<div
31+
css={styles.card}
32+
{...divProps}
33+
role="button"
34+
tabIndex={0}
35+
onClick={()=>navigate(templatePageLink)}
36+
onKeyDown={handleKeyDown}
37+
>
38+
<divcss={styles.header}>
39+
<div>
40+
<AvatarData
41+
title={
42+
template.display_name.length>0
43+
?template.display_name
44+
:template.name
45+
}
46+
subtitle={template.organization_display_name}
47+
avatar={
48+
hasIcon&&(
49+
<ExternalAvatarvariant="square"fitImagesrc={template.icon}/>
50+
)
51+
}
52+
/>
53+
</div>
54+
<div>
55+
{template.active_user_count}{" "}
56+
{template.active_user_count===1 ?"user" :"users"}
57+
</div>
58+
</div>
59+
60+
<div>
61+
<spancss={styles.description}>
62+
<p>{template.description}</p>
63+
</span>
64+
</div>
65+
66+
<divcss={styles.useButtonContainer}>
67+
{template.deprecated ?(
68+
<DeprecatedBadge/>
69+
) :(
70+
<Button
71+
component={RouterLink}
72+
css={styles.actionButton}
73+
className="actionButton"
74+
fullWidth
75+
startIcon={<ArrowForwardOutlined/>}
76+
title={`Create a workspace using the${template.display_name} template`}
77+
to={`/templates/${template.name}/workspace`}
78+
onClick={(e)=>{
79+
e.stopPropagation();
80+
}}
81+
>
82+
Create Workspace
83+
</Button>
84+
)}
85+
</div>
86+
</div>
87+
);
88+
};
89+
90+
conststyles={
91+
card:(theme)=>({
92+
width:"320px",
93+
padding:24,
94+
borderRadius:6,
95+
border:`1px solid${theme.palette.divider}`,
96+
textAlign:"left",
97+
color:"inherit",
98+
display:"flex",
99+
flexDirection:"column",
100+
cursor:"pointer",
101+
"&:hover":{
102+
color:theme.experimental.l2.hover.text,
103+
borderColor:theme.experimental.l2.hover.text,
104+
},
105+
}),
106+
107+
header:{
108+
display:"flex",
109+
alignItems:"center",
110+
justifyContent:"space-between",
111+
marginBottom:24,
112+
},
113+
114+
icon:{
115+
flexShrink:0,
116+
paddingTop:4,
117+
width:32,
118+
height:32,
119+
},
120+
121+
description:(theme)=>({
122+
fontSize:13,
123+
color:theme.palette.text.secondary,
124+
lineHeight:"1.6",
125+
display:"block",
126+
}),
127+
128+
useButtonContainer:{
129+
display:"flex",
130+
gap:12,
131+
flexDirection:"column",
132+
paddingTop:24,
133+
marginTop:"auto",
134+
alignItems:"center",
135+
},
136+
137+
actionButton:(theme)=>({
138+
transition:"none",
139+
color:theme.palette.text.secondary,
140+
"&:hover":{
141+
borderColor:theme.palette.text.primary,
142+
},
143+
}),
144+
}satisfiesRecord<string,Interpolation<Theme>>;

‎site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { templateExamples } from "api/queries/templates";
55
importtype{TemplateExample}from"api/typesGenerated";
66
import{useDashboard}from"modules/dashboard/useDashboard";
77
import{pageTitle}from"utils/page";
8-
import{getTemplatesByTag}from"utils/starterTemplates";
8+
import{getTemplatesByTag}from"utils/templateAggregators";
99
import{StarterTemplatesPageView}from"./StarterTemplatesPageView";
1010

1111
constStarterTemplatesPage:FC=()=>{

‎site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
MockTemplateExample,
66
MockTemplateExample2,
77
}from"testHelpers/entities";
8-
import{getTemplatesByTag}from"utils/starterTemplates";
8+
import{getTemplatesByTag}from"utils/templateAggregators";
99
import{StarterTemplatesPageView}from"./StarterTemplatesPageView";
1010

1111
constmeta:Meta<typeofStarterTemplatesPageView>={

‎site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
}from"components/PageHeader/PageHeader";
1212
import{Stack}from"components/Stack/Stack";
1313
import{TemplateExampleCard}from"modules/templates/TemplateExampleCard/TemplateExampleCard";
14-
importtype{StarterTemplatesByTag}from"utils/starterTemplates";
14+
importtype{StarterTemplatesByTag}from"utils/templateAggregators";
1515

1616
constgetTagLabel=(tag:string)=>{
1717
constlabelByTag:Record<string,string>={

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp