Skip to content
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

Improve UX of project SSO config form #737

Merged
merged 1 commit into from
Sep 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 9 additions & 5 deletions pkg/app/web/src/api/project.ts
Expand Up @@ -78,16 +78,20 @@ export const updateGitHubSSO = ({
clientSecret,
baseUrl,
uploadUrl,
}: ProjectSSOConfig.GitHub.AsObject): Promise<
UpdateProjectSSOConfigResponse.AsObject
> => {
}: {
clientId: string;
clientSecret: string;
baseUrl?: string;
uploadUrl?: string;
}): Promise<UpdateProjectSSOConfigResponse.AsObject> => {
const req = new UpdateProjectSSOConfigRequest();
const sso = new ProjectSSOConfig();
const github = new ProjectSSOConfig.GitHub();
github.setClientId(clientId);
github.setClientSecret(clientSecret);
github.setBaseUrl(baseUrl);
github.setUploadUrl(uploadUrl);
baseUrl && github.setBaseUrl(baseUrl);
uploadUrl && github.setUploadUrl(uploadUrl);

sso.setGithub(github);
req.setSso(sso);
return apiRequest(req, apiClient.updateProjectSSOConfig);
Expand Down
11 changes: 1 addition & 10 deletions pkg/app/web/src/components/github-sso-form.stories.tsx
@@ -1,18 +1,9 @@
import React from "react";
import { GithubSSOForm } from "./github-sso-form";
import { action } from "@storybook/addon-actions";

export default {
title: "SETTINGS/GithubSSOForm",
component: GithubSSOForm,
};

export const overview: React.FC = () => (
<GithubSSOForm
onSave={(params) => {
action("onSave")(params);
return Promise.resolve();
}}
isSaving={false}
/>
);
export const overview: React.FC = () => <GithubSSOForm />;
247 changes: 111 additions & 136 deletions pkg/app/web/src/components/github-sso-form.tsx
@@ -1,13 +1,33 @@
import { IconButton, makeStyles, Typography } from "@material-ui/core";
import EditIcon from "@material-ui/icons/Edit";
import React, { FC, memo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "../modules";
import {
Button,
TextField,
Typography,
Dialog,
DialogContent,
DialogTitle,
DialogActions,
} from "@material-ui/core";
import React, { FC, useState } from "react";
fetchProject,
GitHubSSO,
Teams,
updateGitHubSSO,
updateRBAC,
} from "../modules/project";
import { AppDispatch } from "../store";
import { InputForm } from "./input-form";
import { SSOEditDialog } from "./sso-edit-dialog";

const useStyles = makeStyles((theme) => ({
indent: {
padding: theme.spacing(1),
},
name: {
color: theme.palette.text.secondary,
marginRight: theme.spacing(2),
minWidth: 120,
},
item: {
display: "flex",
alignItems: "center",
},
}));

export interface GitHubSSOFormParams {
clientId: string;
Expand All @@ -20,142 +40,97 @@ export interface GitHubSSOFormParams {
viewerTeam: string;
}

interface Props {
isSaving: boolean;
onSave: (params: GitHubSSOFormParams) => Promise<unknown>;
}

function hasEmptyValue(obj: Record<string, string>): boolean {
return Object.keys(obj).some((key) => obj[key] === "");
}

const initialParams = {
clientId: "",
clientSecret: "",
baseUrl: "",
uploadUrl: "",
org: "",
adminTeam: "",
editorTeam: "",
viewerTeam: "",
};

export const GithubSSOForm: FC<Props> = ({ isSaving, onSave }) => {
const [isOpen, setIsOpen] = useState(false);
const [params, setParams] = useState(initialParams);

const hasEmpty = hasEmptyValue(params);
export const GithubSSOForm: FC = memo(function GithubSSOForm() {
const classes = useStyles();
const dispatch = useDispatch<AppDispatch>();
const [isEditSSO, setIsEditSSO] = useState(false);
const teams = useSelector<AppState, Teams | null>(
(state) => state.project.teams
);
const sso = useSelector<AppState, GitHubSSO | null>(
(state) => state.project.github
);

const handleOnSave = (): void => {
onSave(params).then(() => {
setIsOpen(false);
setParams(initialParams);
const handleSaveTeams = (params: Partial<Teams>): void => {
dispatch(updateRBAC(params)).finally(() => {
dispatch(fetchProject());
});
};

const handleCancel = (): void => {
setIsOpen(false);
setParams(initialParams);
const handleSaveSSO = (
params: Partial<GitHubSSO> & { clientId: string; clientSecret: string }
): void => {
dispatch(updateGitHubSSO(params)).finally(() => {
dispatch(fetchProject());
});
};

return (
<div>
<>
<Typography variant="h6">GitHub</Typography>
<Button variant="contained" onClick={() => setIsOpen(true)}>
EDIT
</Button>

<Dialog open={isOpen}>
<DialogTitle>GitHub Single Sign On Setting</DialogTitle>
<DialogContent>
<TextField
label="Client ID"
margin="dense"
variant="outlined"
fullWidth
value={params.clientId}
onChange={(e) => setParams({ ...params, clientId: e.target.value })}
/>
<TextField
label="Client Secret"
margin="dense"
variant="outlined"
fullWidth
value={params.clientSecret}
onChange={(e) =>
setParams({ ...params, clientSecret: e.target.value })
}
/>
<TextField
label="Base URL"
margin="dense"
variant="outlined"
fullWidth
value={params.baseUrl}
onChange={(e) => setParams({ ...params, baseUrl: e.target.value })}
/>
<TextField
label="Upload URL"
margin="dense"
variant="outlined"
fullWidth
value={params.uploadUrl}
onChange={(e) =>
setParams({ ...params, uploadUrl: e.target.value })
}
<div className={classes.indent}>
<Typography variant="subtitle2">Team</Typography>
<div className={classes.indent}>
<InputForm
currentValue={teams?.admin}
name="Admin Team"
onSave={(value) => handleSaveTeams({ admin: value })}
/>
<TextField
label="Organization"
margin="dense"
variant="outlined"
fullWidth
value={params.org}
onChange={(e) => setParams({ ...params, org: e.target.value })}
<InputForm
currentValue={teams?.editor}
name="Editor Team"
onSave={(value) => handleSaveTeams({ editor: value })}
/>
<TextField
label="Admin Team"
margin="dense"
variant="outlined"
fullWidth
value={params.adminTeam}
onChange={(e) =>
setParams({ ...params, adminTeam: e.target.value })
}
<InputForm
currentValue={teams?.viewer}
name="Viewer Team"
onSave={(value) => handleSaveTeams({ viewer: value })}
/>
<TextField
label="Editor Team"
margin="dense"
variant="outlined"
fullWidth
value={params.editorTeam}
onChange={(e) =>
setParams({ ...params, editorTeam: e.target.value })
}
/>
<TextField
label="Viewer Team"
margin="dense"
variant="outlined"
fullWidth
value={params.viewerTeam}
onChange={(e) =>
setParams({ ...params, viewerTeam: e.target.value })
}
/>
<DialogActions>
<Button color="primary" onClick={handleCancel} disabled={isSaving}>
CANCEL
</Button>
<Button
color="primary"
onClick={handleOnSave}
disabled={isSaving || hasEmpty}
>
SAVE
</Button>
</DialogActions>
</DialogContent>
</Dialog>
</div>
</div>
</div>

<div className={classes.indent}>
<Typography variant="subtitle2">
SSO
<IconButton onClick={() => setIsEditSSO(true)}>
<EditIcon />
</IconButton>
<div className={classes.indent}>
<div className={classes.item}>
<Typography variant="subtitle1" className={classes.name}>
Client ID
</Typography>
<Typography variant="body1">{sso?.clientId}</Typography>
</div>

<div className={classes.item}>
<Typography variant="subtitle1" className={classes.name}>
Client Secret
</Typography>
<Typography variant="body1">{sso?.clientSecret}</Typography>
</div>
<div className={classes.item}>
<Typography variant="subtitle1" className={classes.name}>
Base URL
</Typography>
<Typography variant="body1">{sso?.baseUrl}</Typography>
</div>
<div className={classes.item}>
<Typography variant="subtitle1" className={classes.name}>
Upload URL
</Typography>
<Typography variant="body1">{sso?.uploadUrl}</Typography>
</div>
</div>
</Typography>
</div>
<SSOEditDialog
currentBaseURL={sso?.baseUrl ?? ""}
currentUploadURL={sso?.uploadUrl ?? ""}
onSave={handleSaveSSO}
open={isEditSSO}
onClose={() => setIsEditSSO(false)}
/>
</>
);
};
});
21 changes: 21 additions & 0 deletions pkg/app/web/src/components/input-form.stories.tsx
@@ -0,0 +1,21 @@
import React from "react";
import { InputForm } from "./input-form";
import { action } from "@storybook/addon-actions";

export default {
title: "InputForm",
component: InputForm,
};

export const overview: React.FC = () => (
<InputForm name="Name" currentValue="value" onSave={action("onChange")} />
);

export const IsSecret: React.FC = () => (
<InputForm
name="Name"
currentValue="value"
onSave={action("onChange")}
isSecret
/>
);