Skip to content

Commit

Permalink
Implement project role functionality (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
dthomson25 authored Sep 14, 2018
1 parent eff5421 commit 3e2f205
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 26 deletions.
148 changes: 124 additions & 24 deletions src/app/settings/components/project-details/project-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { RouteComponentProps } from 'react-router';
import { DataLoader, ErrorNotification, Page, Query } from '../../../shared/components';
import { Consumer } from '../../../shared/context';
import { Project } from '../../../shared/models';
import { services } from '../../../shared/services';
import { ProjectRoleParams, services } from '../../../shared/services';

import { ProjectEditPanel } from '../project-edit-panel/project-edit-panel';
import { ProjectEvents } from '../project-events/project-events';
import { ProjectRoleEditPanel } from '../project-role-edit-panel/project-role-edit-panel';

export class ProjectDetails extends React.Component<RouteComponentProps<{ name: string; }>> {
private formApi: FormApi;
private projectFormApi: FormApi;
private projectRoleFormApi: FormApi;
private loader: DataLoader;

public render() {
Expand All @@ -22,6 +24,13 @@ export class ProjectDetails extends React.Component<RouteComponentProps<{ name:
<Page title='Projects' toolbar={{
breadcrumbs: [{title: 'Settings', path: '/settings' }, {title: 'Projects', path: '/settings/projects'}, {title: this.props.match.params.name}],
actionMenu: {items: [
{ title: 'Add Role', iconClassName: 'icon fa fa-plus', action: () => {
this.projectRoleFormApi.setValue('roleName', '');
this.projectRoleFormApi.setValue('description', '');
this.projectRoleFormApi.setValue('policies', '');
this.projectRoleFormApi.setValue('jwtTokens', []);
ctx.navigation.goto('.', {newRole: true});
}},
{ title: 'Edit', iconClassName: 'icon fa fa-pencil', action: () => ctx.navigation.goto('.', {edit: true}) },
{ title: 'Delete', iconClassName: 'icon fa fa-times-circle', action: async () => {
const confirmed = await ctx.popup.confirm('Delete project', 'Are you sure you want to delete project?');
Expand Down Expand Up @@ -52,38 +61,94 @@ export class ProjectDetails extends React.Component<RouteComponentProps<{ name:
key: 'events',
title: 'Events',
content: this.eventsTab(proj),
}, {
key: 'roles',
title: 'Roles',
content: this.rolesTab(proj, ctx),
}]}/>
<SlidingPanel isMiddle={true} isShown={params.get('edit') === 'true'} onClose={() => ctx.navigation.goto('.', {edit: null})} header={(
<SlidingPanel isMiddle={true} isShown={params.get('edit') === 'true'}
onClose={() => ctx.navigation.goto('.', {edit: null})} header={(
<div>
<button onClick={() => ctx.navigation.goto('.', {edit: null})} className='argo-button argo-button--base-o'>
Cancel
</button> <button onClick={() => this.formApi.submitForm(null)} className='argo-button argo-button--base'>
</button> <button onClick={() => this.projectFormApi.submitForm(null)} className='argo-button argo-button--base'>
Update
</button>
</div>
)}>
<ProjectEditPanel nameReadonly={true} defaultParams={{
name: proj.metadata.name,
description: proj.spec.description,
destinations: proj.spec.destinations || [],
sourceRepos: proj.spec.sourceRepos || [],
}} getApi={(api) => this.formApi = api} submit={async (projParams) => {
try {
await services.projects.update(projParams);
ctx.navigation.goto('.', {edit: null});
this.loader.reload();
} catch (e) {
ctx.notifications.show({
content: <ErrorNotification title='Unable to edit project' e={e}/>,
type: NotificationType.Error,
});
}
}}/>
</SlidingPanel>
<ProjectEditPanel nameReadonly={true} defaultParams={{
name: proj.metadata.name,
description: proj.spec.description,
destinations: proj.spec.destinations || [],
sourceRepos: proj.spec.sourceRepos || [],
roles: proj.spec.roles || [],
}} getApi={(api) => this.projectRoleFormApi = api} submit={async (projParams) => {
try {
await services.projects.update(projParams);
ctx.navigation.goto('.', {edit: null});
this.loader.reload();
} catch (e) {
ctx.notifications.show({
content: <ErrorNotification title='Unable to edit project' e={e}/>,
type: NotificationType.Error,
});
}
}
}/>
</SlidingPanel>
<SlidingPanel isMiddle={true} isShown={params.get('editRole') !== null || params.get('newRole') !== null}
onClose={() => ctx.navigation.goto('.', {editRole: null, newRole: null})} header={(
<div>
<button onClick={() => ctx.navigation.goto('.', {editRole: null, newRole: null})} className='argo-button argo-button--base-o'>
Cancel
</button> <button onClick={() => this.projectRoleFormApi.submitForm(null)} className='argo-button argo-button--base'>
{params.get('newRole') != null ? 'Create' : 'Update'}
</button> {params.get('newRole') === null ? (
<button onClick={async () => {
const confirmed = await ctx.popup.confirm('Delete project role', 'Are you sure you want to delete project role?');
if (confirmed) {
try {
this.projectRoleFormApi.setValue('deleteRole', true);
this.projectRoleFormApi.submitForm(null);
ctx.navigation.goto('.', {editRole: null});
} catch (e) {
ctx.notifications.show({
content: <ErrorNotification title='Unable to delete project role' e={e}/>,
type: NotificationType.Error,
});
}
}
}} className='argo-button argo-button--base'>
Delete
</button>
) : null}
</div>
)}>
<ProjectRoleEditPanel nameReadonly={params.get('newRole') === null ? true : false}
defaultParams={{
deleteRole: false,
projName: proj.metadata.name,
role: (params.get('newRole') === null && proj.spec.roles !== undefined) ?
proj.spec.roles.find((x) => params.get('editRole') === x.name)
: undefined,
}}
getApi={(api: FormApi) => this.projectRoleFormApi = api} submit={async (projRoleParams: ProjectRoleParams) => {
try {
await services.projects.updateRole(projRoleParams);
ctx.navigation.goto('.', {editRole: null, newRole: null});
this.loader.reload();
} catch (e) {
ctx.notifications.show({
content: <ErrorNotification title='Unable to edit project' e={e}/>,
type: NotificationType.Error,
});
}
}}
/>
</SlidingPanel>
</div>
)}
</Query>

)}
</DataLoader>
</Page>
Expand All @@ -100,6 +165,41 @@ export class ProjectDetails extends React.Component<RouteComponentProps<{ name:
);
}

private rolesTab(proj: Project, ctx: any) {
return (
<div className='argo-container'>
{(proj.spec.roles || []).length > 0 && (
<div className='argo-table-list argo-table-list--clickable'>
<div className='argo-table-list__head'>
<div className='row'>
<div className='columns small-3'>NAME</div>
<div className='columns small-6'>DESCRIPTION</div>
</div>
</div>
{(proj.spec.roles || []).map((role) => (
<div className='argo-table-list__row' key={`${role.name}`}
onClick={() => {
this.projectRoleFormApi.setValue('roleName', role.name);
this.projectRoleFormApi.setValue('description', role.description);
this.projectRoleFormApi.setValue('policies', role.policies !== null ? role.policies.join('\n') : '');
this.projectRoleFormApi.setValue('jwtTokens', role.jwtTokens);
ctx.navigation.goto(`.`, {editRole: role.name});
}}>
<div className='row'>
<div className='columns small-3'>
{role.name}
</div>
<div className='columns small-6'>
{role.description}
</div>
</div>
</div>
))}
</div>) || <div className='white-box'><p>Project has no roles</p></div>}
</div>
);
}

private summaryTab(proj: Project) {
const attributes = [
{title: 'NAME', value: proj.metadata.name},
Expand Down Expand Up @@ -161,7 +261,7 @@ export class ProjectDetails extends React.Component<RouteComponentProps<{ name:
</div>
))}
</div>) || <div className='white-box'><p>Project has no destinations</p></div>}
</div>
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const ProjectEditPanel = (props: {
<Form
onSubmit={props.submit}
getApi={props.getApi}
defaultValues={{sourceRepos: [], destinations: [], ...props.defaultParams}}
defaultValues={{sourceRepos: [], destinations: [], roles: [], ...props.defaultParams}}
validateError={(params: ProjectParams) => ({
name: !params.name && 'Project name is required',
})}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as React from 'react';
import { Form, FormApi, Text, TextArea } from 'react-form';

import { FormField } from '../../../shared/components';
import * as models from '../../../shared/models';
import { ProjectRoleParams } from '../../../shared/services';

interface ProjectRoleDefaultParams {
projName: string;
role?: models.ProjectRole;
deleteRole: boolean;
}

interface ProjectRoleEditPanelProps {
nameReadonly?: boolean;
submit: (params: ProjectRoleParams) => any;
getApi?: (formApi: FormApi) => void;
defaultParams: ProjectRoleDefaultParams;
}

export class ProjectRoleEditPanel extends React.Component<ProjectRoleEditPanelProps, any> {

public render() {
return (
<div className='project-role-edit-panel'>
<Form
onSubmit={this.props.submit}
getApi={this.props.getApi}
defaultValues={{
projName: this.props.defaultParams.projName,
roleName: (this.props.defaultParams.role !== undefined ? this.props.defaultParams.role.name : ''),
description: (this.props.defaultParams.role !== undefined ? this.props.defaultParams.role.description : ''),
policies: (this.props.defaultParams.role !== undefined && this.props.defaultParams.role.policies !== null
? this.props.defaultParams.role.policies.join('\n') : ''),
jwtTokens: (this.props.defaultParams.role !== undefined ? this.props.defaultParams.role.jwtTokens : []),
}}
validateError={(params: ProjectRoleParams) => ({
projName: !params.projName && 'Project name is required',
roleName: !params.roleName && 'Role name is required',
})
}>
{(api) => (
<form onSubmit={api.submitForm} role='form' className='width-control'>
<div className='argo-form-row'>
<FormField formApi={api} label='Role Name'
componentProps={{ readOnly: this.props.nameReadonly }} field='roleName' component={Text}/>
</div>
<div className='argo-form-row'>
<FormField formApi={api} label='Role Description' field='description' component={Text}/>
</div>
<h4>Policies:</h4>
<FormField formApi={api} label='' field='policies' component={TextArea}/>
<h4>JWT Tokens:</h4>
{ api.values.jwtTokens !== null && api.values.jwtTokens.length > 0 ? (
<div className='argo-table-list'>
<div className='argo-table-list__head'>
<div className='row'>
<div className='columns small-3'>ID</div>
<div className='columns small-3'>ISSUED AT</div>
<div className='columns small-3'>EXPIRES AT</div>
</div>
</div>
{api.values.jwtTokens.map((jwtToken: models.JwtToken) => (
<div className='argo-table-list__row' key={`${jwtToken.iat}`}>
<div className='row'>
<div className='columns small-3'>
{jwtToken.iat}
</div>
<div className='columns small-3'>
{new Date(jwtToken.iat * 1000).toDateString()}
</div>
<div className='columns small-3'>
{jwtToken.exp == null ? 'None' : new Date(jwtToken.exp * 1000).toDateString()}
</div>
</div>
</div>
))}
</div>
) : <div className='white-box'><p>Role has no JWT tokens</p></div> }
</form>
)}
</Form>
</div>
);
}
}
12 changes: 12 additions & 0 deletions src/app/shared/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,22 @@ export interface Event {

export interface EventList extends ItemsList<Event> {}

export interface ProjectRole {
description: string;
policies: string[];
name: string;
jwtTokens: JwtToken[];
}

export interface JwtToken {
iat: number;
exp: number;
}
export interface ProjectSpec {
sourceRepos: string[];
destinations: ApplicationDestination[];
description: string;
roles: ProjectRole[];
}

export interface Project {
Expand Down
2 changes: 1 addition & 1 deletion src/app/shared/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ export const services: Services = {
viewPreferences: new ViewPreferencesService(),
};

export { ProjectParams } from './projects-service';
export { ProjectParams, ProjectRoleParams } from './projects-service';
Loading

0 comments on commit 3e2f205

Please sign in to comment.