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

Commita3ebcd7

Browse files
authored
feat: integrate backend with idp sync page (#14755)
* feat: idp sync initial commit* fix: hookup backend data for groups and roles* chore: cleanup* feat: separate groups and roles into tabs* feat: implement export policy button* feat: handle missing groups* chore: add story for missing groups* chore: add stories for export policy button* fix: updates for PR review* chore: update tests* chore: document uuid regex* chore: remove unused* fix: fix stories
1 parentb4f54f3 commita3ebcd7

File tree

12 files changed

+654
-207
lines changed

12 files changed

+654
-207
lines changed

‎site/src/api/api.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,30 @@ class ApiMethods {
704704
returnresponse.data;
705705
};
706706

707+
/**
708+
*@param organization Can be the organization's ID or name
709+
*/
710+
getGroupIdpSyncSettingsByOrganization=async(
711+
organization:string,
712+
):Promise<TypesGen.GroupSyncSettings>=>{
713+
constresponse=awaitthis.axios.get<TypesGen.GroupSyncSettings>(
714+
`/api/v2/organizations/${organization}/settings/idpsync/groups`,
715+
);
716+
returnresponse.data;
717+
};
718+
719+
/**
720+
*@param organization Can be the organization's ID or name
721+
*/
722+
getRoleIdpSyncSettingsByOrganization=async(
723+
organization:string,
724+
):Promise<TypesGen.RoleSyncSettings>=>{
725+
constresponse=awaitthis.axios.get<TypesGen.RoleSyncSettings>(
726+
`/api/v2/organizations/${organization}/settings/idpsync/roles`,
727+
);
728+
returnresponse.data;
729+
};
730+
707731
getTemplate=async(templateId:string):Promise<TypesGen.Template>=>{
708732
constresponse=awaitthis.axios.get<TypesGen.Template>(
709733
`/api/v2/templates/${templateId}`,

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,32 @@ export const provisionerDaemonGroups = (organization: string) => {
141141
};
142142
};
143143

144+
exportconstgetGroupIdpSyncSettingsKey=(organization:string)=>[
145+
"organizations",
146+
organization,
147+
"groupIdpSyncSettings",
148+
];
149+
150+
exportconstgroupIdpSyncSettings=(organization:string)=>{
151+
return{
152+
queryKey:getGroupIdpSyncSettingsKey(organization),
153+
queryFn:()=>API.getGroupIdpSyncSettingsByOrganization(organization),
154+
};
155+
};
156+
157+
exportconstgetRoleIdpSyncSettingsKey=(organization:string)=>[
158+
"organizations",
159+
organization,
160+
"roleIdpSyncSettings",
161+
];
162+
163+
exportconstroleIdpSyncSettings=(organization:string)=>{
164+
return{
165+
queryKey:getRoleIdpSyncSettingsKey(organization),
166+
queryFn:()=>API.getRoleIdpSyncSettingsByOrganization(organization),
167+
};
168+
};
169+
144170
/**
145171
* Fetch permissions for a single organization.
146172
*
@@ -243,6 +269,13 @@ export const organizationsPermissions = (
243269
},
244270
action:"read",
245271
},
272+
viewIdpSyncSettings:{
273+
object:{
274+
resource_type:"idpsync_settings",
275+
organization_id:organizationId,
276+
},
277+
action:"read",
278+
},
246279
});
247280

248281
// The endpoint takes a flat array, so to avoid collisions prepend each

‎site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const AppearanceSettingsPageView: FC<
7474
<PopoverContentcss={{transform:"translateY(-28px)"}}>
7575
<PopoverPaywall
7676
message="Appearance"
77-
description="With a Premium license, you can customize the appearance of your deployment."
77+
description="With a Premium license, you can customize the appearanceand brandingof your deployment."
7878
documentationLink="https://coder.com/docs/admin/appearance"
7979
/>
8080
</PopoverContent>

‎site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
2929
import{docs}from"utils/docs";
3030
import{PermissionPillsList}from"./PermissionPillsList";
3131

32-
exporttypeCustomRolesPageViewProps={
32+
interfaceCustomRolesPageViewProps{
3333
roles:Role[]|undefined;
3434
onDeleteRole:(role:Role)=>void;
3535
canAssignOrgRole:boolean;
3636
isCustomRolesEnabled:boolean;
37-
};
37+
}
3838

3939
exportconstCustomRolesPageView:FC<CustomRolesPageViewProps>=({
4040
roles,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
importtype{Meta,StoryObj}from"@storybook/react";
2+
import{expect,fn,userEvent,waitFor,within}from"@storybook/test";
3+
import{
4+
MockGroupSyncSettings,
5+
MockOrganization,
6+
MockRoleSyncSettings,
7+
}from"testHelpers/entities";
8+
import{ExportPolicyButton}from"./ExportPolicyButton";
9+
10+
constmeta:Meta<typeofExportPolicyButton>={
11+
title:"modules/resources/ExportPolicyButton",
12+
component:ExportPolicyButton,
13+
args:{
14+
syncSettings:MockGroupSyncSettings,
15+
type:"groups",
16+
organization:MockOrganization,
17+
},
18+
};
19+
20+
exportdefaultmeta;
21+
typeStory=StoryObj<typeofExportPolicyButton>;
22+
23+
exportconstDefault:Story={};
24+
25+
exportconstClickExportGroupPolicy:Story={
26+
args:{
27+
syncSettings:MockGroupSyncSettings,
28+
type:"groups",
29+
organization:MockOrganization,
30+
download:fn(),
31+
},
32+
play:async({ canvasElement, args})=>{
33+
constcanvas=within(canvasElement);
34+
awaituserEvent.click(
35+
canvas.getByRole("button",{name:"Export Policy"}),
36+
);
37+
awaitwaitFor(()=>
38+
expect(args.download).toHaveBeenCalledWith(
39+
expect.anything(),
40+
`${MockOrganization.name}_groups-policy.json`,
41+
),
42+
);
43+
constblob:Blob=(args.downloadasjest.Mock).mock.lastCall[0];
44+
awaitexpect(blob.type).toEqual("application/json");
45+
awaitexpect(awaitblob.text()).toEqual(
46+
JSON.stringify(MockGroupSyncSettings,null,2),
47+
);
48+
},
49+
};
50+
51+
exportconstClickExportRolePolicy:Story={
52+
args:{
53+
syncSettings:MockRoleSyncSettings,
54+
type:"roles",
55+
organization:MockOrganization,
56+
download:fn(),
57+
},
58+
play:async({ canvasElement, args})=>{
59+
constcanvas=within(canvasElement);
60+
awaituserEvent.click(
61+
canvas.getByRole("button",{name:"Export Policy"}),
62+
);
63+
awaitwaitFor(()=>
64+
expect(args.download).toHaveBeenCalledWith(
65+
expect.anything(),
66+
`${MockOrganization.name}_roles-policy.json`,
67+
),
68+
);
69+
constblob:Blob=(args.downloadasjest.Mock).mock.lastCall[0];
70+
awaitexpect(blob.type).toEqual("application/json");
71+
awaitexpect(awaitblob.text()).toEqual(
72+
JSON.stringify(MockRoleSyncSettings,null,2),
73+
);
74+
},
75+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
importDownloadOutlinedfrom"@mui/icons-material/DownloadOutlined";
2+
importButtonfrom"@mui/material/Button";
3+
importtype{
4+
GroupSyncSettings,
5+
Organization,
6+
RoleSyncSettings,
7+
}from"api/typesGenerated";
8+
import{displayError}from"components/GlobalSnackbar/utils";
9+
import{saveAs}from"file-saver";
10+
import{typeFC,useMemo,useState}from"react";
11+
12+
interfaceDownloadPolicyButtonProps{
13+
syncSettings:RoleSyncSettings|GroupSyncSettings|undefined;
14+
type:"groups"|"roles";
15+
organization:Organization;
16+
download?:(file:Blob,filename:string)=>void;
17+
}
18+
19+
exportconstExportPolicyButton:FC<DownloadPolicyButtonProps>=({
20+
syncSettings,
21+
type,
22+
organization,
23+
download=saveAs,
24+
})=>{
25+
const[isDownloading,setIsDownloading]=useState(false);
26+
27+
constpolicyJSON=useMemo(()=>{
28+
returnsyncSettings?.field&&syncSettings.mapping
29+
?JSON.stringify(syncSettings,null,2)
30+
:null;
31+
},[syncSettings]);
32+
33+
return(
34+
<Button
35+
startIcon={<DownloadOutlined/>}
36+
disabled={!policyJSON||isDownloading}
37+
onClick={async()=>{
38+
if(policyJSON){
39+
try{
40+
setIsDownloading(true);
41+
constfile=newBlob([policyJSON],{
42+
type:"application/json",
43+
});
44+
download(file,`${organization.name}_${type}-policy.json`);
45+
}catch(e){
46+
console.error(e);
47+
displayError("Failed to export policy json");
48+
}finally{
49+
setIsDownloading(false);
50+
}
51+
}
52+
}}
53+
>
54+
Export Policy
55+
</Button>
56+
);
57+
};
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import{typeInterpolation,typeTheme,useTheme}from"@emotion/react";
2+
importStackfrom"@mui/material/Stack";
3+
import{Pill}from"components/Pill/Pill";
4+
import{
5+
Popover,
6+
PopoverContent,
7+
PopoverTrigger,
8+
}from"components/Popover/Popover";
9+
importtype{FC}from"react";
10+
11+
interfacePillListProps{
12+
roles:readonlystring[];
13+
}
14+
15+
// used to check if the role is a UUID
16+
constUUID=
17+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
18+
19+
exportconstIdpPillList:FC<PillListProps>=({ roles})=>{
20+
return(
21+
<Stackdirection="row"spacing={1}>
22+
{roles.length>0 ?(
23+
<Pillcss={UUID.test(roles[0]) ?styles.errorPill :styles.pill}>
24+
{roles[0]}
25+
</Pill>
26+
) :(
27+
<p>None</p>
28+
)}
29+
30+
{roles.length>1&&<OverflowPillroles={roles.slice(1)}/>}
31+
</Stack>
32+
);
33+
};
34+
35+
interfaceOverflowPillProps{
36+
roles:string[];
37+
}
38+
39+
constOverflowPill:FC<OverflowPillProps>=({ roles})=>{
40+
consttheme=useTheme();
41+
42+
return(
43+
<Popovermode="hover">
44+
<PopoverTrigger>
45+
<Pill
46+
css={{
47+
backgroundColor:theme.palette.background.paper,
48+
borderColor:theme.palette.divider,
49+
}}
50+
data-testid="overflow-pill"
51+
>
52+
+{roles.length} more
53+
</Pill>
54+
</PopoverTrigger>
55+
56+
<PopoverContent
57+
disableRestoreFocus
58+
disableScrollLock
59+
css={{
60+
".MuiPaper-root":{
61+
display:"flex",
62+
flexFlow:"column wrap",
63+
columnGap:8,
64+
rowGap:12,
65+
padding:"12px 16px",
66+
alignContent:"space-around",
67+
minWidth:"auto",
68+
backgroundColor:theme.palette.background.default,
69+
},
70+
}}
71+
anchorOrigin={{
72+
vertical:-4,
73+
horizontal:"center",
74+
}}
75+
transformOrigin={{
76+
vertical:"bottom",
77+
horizontal:"center",
78+
}}
79+
>
80+
{roles.map((role)=>(
81+
<Pill
82+
key={role}
83+
css={UUID.test(role) ?styles.errorPill :styles.pill}
84+
>
85+
{role}
86+
</Pill>
87+
))}
88+
</PopoverContent>
89+
</Popover>
90+
);
91+
};
92+
93+
conststyles={
94+
pill:(theme)=>({
95+
backgroundColor:theme.experimental.pillDefault.background,
96+
borderColor:theme.experimental.pillDefault.outline,
97+
color:theme.experimental.pillDefault.text,
98+
width:"fit-content",
99+
}),
100+
errorPill:(theme)=>({
101+
backgroundColor:theme.roles.error.background,
102+
borderColor:theme.roles.error.outline,
103+
color:theme.roles.error.text,
104+
width:"fit-content",
105+
}),
106+
}satisfiesRecord<string,Interpolation<Theme>>;

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp