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

Commit5aa54be

Browse files
authored
chore: update workspaces page filter to include organization controls (#14597)
* chore: move schedule controls to the right side of the screen* chore: add org display to workspace topbar* fix: force organizations to be readonly array* fix update type mismatch for organizations again* refactor: tuck main loading skeleton for filter into base definition* refactor: give filter files different names to reduce confusion* refactor: remove separate base filter skeleton* fix: update responsive logic for audit table filter* chore: add organizations option group to workspaces table* refactor: make prop contracts more explicit* refactor: centralize the organizations dropdown logic* fix: update imports and formatting* fix: update quota querying logic to use new endpoint* fix: add logic for handling long workspace or org names* chore: add links for workspaces by org* chore: expand tooltip styling for org* chore: expand tooltip styling for owner* refactor: split off breadcrumbs for readability* fix: display correct template version name in dropdown* fix: update overflow styling for breadcrumb segments* fix: favor org display name* fix: centralize org display name logic* fix: make sure skeletons stay synced with org feature toggles* fix: ensure that mock query cache key and component key are properly synced for storybook* docs: clean up wording on SearchField comment* fix: shrink mix width threshold for search field* chore: add navigation test for workspace details page (#14629)* chore: add tests for WorkspacePage cross-page navigation* fix: update story to use mock organizations menu
1 parent9102256 commit5aa54be

File tree

23 files changed

+342
-202
lines changed

23 files changed

+342
-202
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import{API}from"api/api";
22
importtype{AuditLogResponse}from"api/typesGenerated";
3-
import{useFilterParamsKey}from"components/Filter/filter";
3+
import{useFilterParamsKey}from"components/Filter/Filter";
44
importtype{UsePaginatedQueryOptions}from"hooks/usePaginatedQuery";
55

66
exportfunctionpaginatedAudits(

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,38 +125,40 @@ const BaseSkeleton: FC<SkeletonProps> = ({ children, ...skeletonProps }) => {
125125
);
126126
};
127127

128-
exportconstSearchFieldSkeleton:FC=()=>{
129-
return<BaseSkeletonwidth="100%"/>;
130-
};
131-
132128
exportconstMenuSkeleton:FC=()=>{
133129
return<BaseSkeletoncss={{minWidth:200,flexShrink:0}}/>;
134130
};
135131

136132
typeFilterProps={
137133
filter:ReturnType<typeofuseFilter>;
138-
skeleton:ReactNode;
134+
optionsSkeleton:ReactNode;
139135
isLoading:boolean;
140136
learnMoreLink?:string;
141137
learnMoreLabel2?:string;
142138
learnMoreLink2?:string;
143139
error?:unknown;
144140
options?:ReactNode;
145141
presets:PresetFilter[];
146-
breakpoint?:Breakpoint;
142+
143+
/**
144+
* The CSS media query breakpoint that defines when the UI will try
145+
* displaying all options on one row, regardless of the number of options
146+
* present
147+
*/
148+
singleRowBreakpoint?:Breakpoint;
147149
};
148150

149151
exportconstFilter:FC<FilterProps>=({
150152
filter,
151153
isLoading,
152154
error,
153-
skeleton,
155+
optionsSkeleton,
154156
options,
155157
learnMoreLink,
156158
learnMoreLabel2,
157159
learnMoreLink2,
158160
presets,
159-
breakpoint="md",
161+
singleRowBreakpoint="lg",
160162
})=>{
161163
consttheme=useTheme();
162164
// Storing local copy of the filter query so that it can be updated more
@@ -187,15 +189,18 @@ export const Filter: FC<FilterProps> = ({
187189
display:"flex",
188190
gap:8,
189191
marginBottom:16,
190-
flexWrap:"nowrap",
192+
flexWrap:"wrap",
191193

192-
[theme.breakpoints.down(breakpoint)]:{
193-
flexWrap:"wrap",
194+
[theme.breakpoints.up(singleRowBreakpoint)]:{
195+
flexWrap:"nowrap",
194196
},
195197
}}
196198
>
197199
{isLoading ?(
198-
skeleton
200+
<>
201+
<BaseSkeletonwidth="100%"/>
202+
{optionsSkeleton}
203+
</>
199204
) :(
200205
<>
201206
<InputGroupcss={{width:"100%"}}>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
5252
<SelectMenuTrigger>
5353
<SelectMenuButton
5454
startIcon={selectedOption?.startIcon}
55-
css={{ width,flexGrow:1}}
55+
css={{flexBasis:width,flexGrow:1}}
5656
aria-label={label}
5757
>
5858
{selectedOption?.label??placeholder}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export const useUserFilterMenu = ({
1919
>)=>{
2020
const{user:me}=useAuthenticated();
2121

22-
constaddMeAsFirstOption=(options:SelectFilterOption[])=>{
23-
options=options.filter((option)=>option.value!==me.username);
22+
constaddMeAsFirstOption=(options:readonlySelectFilterOption[])=>{
23+
constfiltered=options.filter((o)=>o.value!==me.username);
2424
return[
2525
{
2626
label:me.username,
@@ -33,7 +33,7 @@ export const useUserFilterMenu = ({
3333
/>
3434
),
3535
},
36-
...options,
36+
...filtered,
3737
];
3838
};
3939

‎site/src/components/Filter/storyHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import{action}from"@storybook/addon-actions";
2-
importtype{UseFilterResult}from"./filter";
2+
importtype{UseFilterResult}from"./Filter";
33
importtype{UseFilterMenuResult}from"./menu";
44

55
exportconstMockMenu:UseFilterMenuResult={

‎site/src/components/SearchField/SearchField.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export const SearchField: FC<SearchFieldProps> = ({
2121
consttheme=useTheme();
2222
return(
2323
<TextField
24+
// Specifying `minWidth` so that the text box can't shrink so much
25+
// that it becomes un-clickable as we add more filter controls
26+
css={{minWidth:"280px"}}
2427
size="small"
2528
value={value}
2629
onChange={(e)=>onChange(e.target.value)}

‎site/src/contexts/auth/RequireAuth.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
import{API}from"api/api";
22
import{isApiError}from"api/errors";
33
import{Loader}from"components/Loader/Loader";
4-
import{ProxyProvider}from"contexts/ProxyContext";
5-
import{DashboardProvider}from"modules/dashboard/DashboardProvider";
4+
import{ProxyProviderasProductionProxyProvider}from"contexts/ProxyContext";
5+
import{DashboardProviderasProductionDashboardProvider}from"modules/dashboard/DashboardProvider";
66
import{typeFC,useEffect}from"react";
77
import{Navigate,Outlet,useLocation}from"react-router-dom";
88
import{embedRedirect}from"utils/redirect";
99
import{typeAuthContextValue,useAuthContext}from"./AuthProvider";
1010

11-
exportconstRequireAuth:FC=()=>{
11+
typeRequireAuthProps=Readonly<{
12+
ProxyProvider?:typeofProductionProxyProvider;
13+
DashboardProvider?:typeofProductionDashboardProvider;
14+
}>;
15+
16+
/**
17+
* Wraps any component and ensures that the user has been authenticated before
18+
* they can access the component's contents.
19+
*
20+
* In production, it is assumed that this component will not be called with any
21+
* props at all. But to make testing easier, you can call this component with
22+
* specific providers to mock them out.
23+
*/
24+
exportconstRequireAuth:FC<RequireAuthProps>=({
25+
DashboardProvider=ProductionDashboardProvider,
26+
ProxyProvider=ProductionProxyProvider,
27+
})=>{
1228
constlocation=useLocation();
1329
const{ signOut, isSigningOut, isSignedOut, isSignedIn, isLoading}=
1430
useAuthContext();
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
*@file Defines a centralized place for filter dropdown groups that are
3+
* relevant across multiple pages within the Coder UI.
4+
*
5+
*@todo 2024-09-06 - Figure out how to move the user dropdown group into this
6+
* file (or whether there are enough subtle differences that it's not worth
7+
* centralizing the logic). We currently have two separate implementations for
8+
* the workspaces and audits page that have a risk of getting out of sync.
9+
*/
10+
import{API}from"api/api";
11+
import{
12+
SelectFilter,
13+
typeSelectFilterOption,
14+
SelectFilterSearch,
15+
}from"components/Filter/SelectFilter";
16+
import{
17+
typeUseFilterMenuOptions,
18+
useFilterMenu,
19+
}from"components/Filter/menu";
20+
import{UserAvatar}from"components/UserAvatar/UserAvatar";
21+
importtype{FC}from"react";
22+
23+
// Organization helpers ////////////////////////////////////////////////////////
24+
25+
exportconstuseOrganizationsFilterMenu=({
26+
value,
27+
onChange,
28+
}:Pick<UseFilterMenuOptions<SelectFilterOption>,"value"|"onChange">)=>{
29+
returnuseFilterMenu({
30+
onChange,
31+
value,
32+
id:"organizations",
33+
getSelectedOption:async()=>{
34+
if(value){
35+
constorganizations=awaitAPI.getOrganizations();
36+
constorganization=organizations.find((o)=>o.name===value);
37+
if(organization){
38+
return{
39+
label:organization.display_name||organization.name,
40+
value:organization.name,
41+
startIcon:(
42+
<UserAvatar
43+
key={organization.id}
44+
size="xs"
45+
username={organization.display_name||organization.name}
46+
avatarURL={organization.icon}
47+
/>
48+
),
49+
};
50+
}
51+
}
52+
returnnull;
53+
},
54+
getOptions:async()=>{
55+
// Only show the organizations for which you can view audit logs.
56+
constorganizations=awaitAPI.getOrganizations();
57+
constpermissions=awaitAPI.checkAuthorization({
58+
checks:Object.fromEntries(
59+
organizations.map((organization)=>[
60+
organization.id,
61+
{
62+
object:{
63+
resource_type:"audit_log",
64+
organization_id:organization.id,
65+
},
66+
action:"read",
67+
},
68+
]),
69+
),
70+
});
71+
returnorganizations
72+
.filter((organization)=>permissions[organization.id])
73+
.map<SelectFilterOption>((organization)=>({
74+
label:organization.display_name||organization.name,
75+
value:organization.name,
76+
startIcon:(
77+
<UserAvatar
78+
key={organization.id}
79+
size="xs"
80+
username={organization.display_name||organization.name}
81+
avatarURL={organization.icon}
82+
/>
83+
),
84+
}));
85+
},
86+
});
87+
};
88+
89+
exporttypeOrganizationsFilterMenu=ReturnType<
90+
typeofuseOrganizationsFilterMenu
91+
>;
92+
93+
interfaceOrganizationsMenuProps{
94+
menu:OrganizationsFilterMenu;
95+
width?:number;
96+
}
97+
98+
exportconstOrganizationsMenu:FC<OrganizationsMenuProps>=({
99+
menu,
100+
width,
101+
})=>{
102+
return(
103+
<SelectFilter
104+
label="Select an organization"
105+
placeholder="All organizations"
106+
emptyText="No organizations found"
107+
options={menu.searchOptions}
108+
onSelect={menu.selectOption}
109+
selectedOption={menu.selectedOption??undefined}
110+
selectFilterSearch={
111+
<SelectFilterSearch
112+
inputProps={{"aria-label":"Search organization"}}
113+
placeholder="Search organization..."
114+
value={menu.query}
115+
onChange={menu.setQuery}
116+
/>
117+
}
118+
width={width}
119+
/>
120+
);
121+
};

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp