Skip to content

Commit c62e52c

Browse files
feat: big update for manage contributors page (#13)
Signed-off-by: Mathew Wicks <[email protected]>
1 parent fbd0d34 commit c62e52c

14 files changed

+445
-269
lines changed

dashboard/app/api_workgroup.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,16 @@ interface EnvironmentInfo {
3737
isClusterAdmin: boolean;
3838
}
3939

40-
export type SimpleRole = 'owner'| 'contributor';
41-
export type WorkgroupRole = 'admin' | 'edit';
40+
export type SimpleRole = 'owner' | 'contributor' | 'viewer';
41+
export type WorkgroupRole = 'admin' | 'edit' | 'view';
4242
export type Role = SimpleRole | WorkgroupRole;
4343
export const roleMap: ReadonlyMap<Role, Role> = new Map([
4444
['admin', 'owner'],
4545
['owner', 'admin'],
4646
['edit', 'contributor'],
4747
['contributor', 'edit'],
48+
['view', 'viewer'],
49+
['viewer', 'view'],
4850
]);
4951

5052
export interface SimpleBinding {
@@ -228,8 +230,8 @@ export class WorkgroupApi {
228230
res.json(users);
229231
} catch (err) {
230232
const errMessage = [
231-
`Unable to add new contributor for ${namespace}: ${err.stack || err}`,
232-
`Unable to fetch contributors for ${namespace}: ${err.stack || err}`,
233+
`Unable to add new contributor for ${namespace}. HTTP ${err.response.statusCode || '???'} - ${err.response.statusMessage || 'Unknown'}`,
234+
`Unable to fetch contributors for ${namespace}. HTTP ${err.response.statusCode || '???'} - ${err.response.statusMessage || 'Unknown'}`,
233235
][errIndex];
234236
surfaceProfileControllerErrors({
235237
res,
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

dashboard/public/components/main-page.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,24 @@ export class MainPage extends utilitiesMixin(PolymerElement) {
497497
// This case is for non-identity networks, that have no namespaces
498498
this._setRegistrationFlow(true);
499499
}
500-
this.ownedNamespace = namespaces.find((n) => n.role == 'owner');
500+
const ownedNamespaces = [];
501+
const editNamespaces = [];
502+
const viewNamespaces = [];
503+
if (this.namespaces.length) {
504+
this.namespaces.forEach((ns) => {
505+
if (ns.role === 'owner') {
506+
ownedNamespaces.push(ns);
507+
} else if (ns.role === 'contributor') {
508+
editNamespaces.push(ns);
509+
} else if (ns.role === 'viewer') {
510+
viewNamespaces.push(ns);
511+
}
512+
});
513+
this.ownedNamespaces = ownedNamespaces;
514+
this.editNamespaces = editNamespaces;
515+
this.viewNamespaces = viewNamespaces;
516+
this.hasNamespaces = true;
517+
}
501518
this.platformInfo = platform;
502519
const kVer = this.platformInfo.kubeflowVersion;
503520
if (kVer && kVer != 'unknown') {

dashboard/public/components/main-page.pug

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ app-drawer-layout.flex(narrow='{{narrowMode}}',
9898
neon-animatable(page='activity')
9999
activity-view(namespace='[[queryParams.ns]]')
100100
neon-animatable(page='manage-users')
101-
manage-users-view(user='[[user]]', namespaces='[[namespaces]]', is-cluster-admin='[[isClusterAdmin]]', owned-namespace='[[ownedNamespace]]')
101+
manage-users-view(
102+
user='[[user]]', namespaces='[[namespaces]]', is-cluster-admin='[[isClusterAdmin]]', has-namespaces='[[hasNamespaces]]',
103+
owned-namespaces='[[ownedNamespaces]]', edit-namespaces='[[editNamespaces]]', view-namespaces='[[viewNamespaces]]')
102104
neon-animatable(page='iframe')
103105
iframe-container(namespace='[[namespace]]',
104106
src='[[iframeSrc]]', page="{{iframePage}}"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
h1, h2 {
2+
font-family: Google Sans;
3+
font-size: 1.2em;
4+
margin: 0;
5+
}
6+
h1 {
7+
color: black;
8+
padding: 1rem;
9+
border-bottom: 1px solid var(--border-color);
10+
}
11+
h2 {
12+
@apply --layout-horizontal;
13+
@apply --layout-center;
14+
font-size: 1em;
15+
font-weight: 500;
16+
color: var(--subheading-color);
17+
margin: .5em 0;
18+
}
19+
h2 .icon {
20+
color: var(--accent-color);
21+
margin-right: 1rem;
22+
/* Because Hamburger icon has a padding area for ripple*/
23+
padding-left: .6rem;
24+
}
25+
.content {
26+
margin-left: calc(1.6rem + 24px);
27+
color: var(--content-color);
28+
font-size: .9em;
29+
}
30+
.content.small {
31+
max-width: 640px;
32+
}
33+
#ContribError {
34+
background: #F44336
35+
}
36+
[hidden] {display: none !important}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import '@polymer/iron-ajax/iron-ajax.js';
2+
import '@polymer/iron-icon/iron-icon.js';
3+
import '@polymer/iron-icons/iron-icons.js';
4+
import '@polymer/iron-icons/social-icons.js';
5+
import '@polymer/paper-toast/paper-toast.js';
6+
import '@polymer/paper-ripple/paper-ripple.js';
7+
import '@polymer/paper-item/paper-icon-item.js';
8+
import '@polymer/paper-icon-button/paper-icon-button.js';
9+
10+
import {html, PolymerElement} from '@polymer/polymer';
11+
12+
import './resources/paper-chip.js';
13+
import './resources/md2-input/md2-input.js';
14+
import css from './manage-users-view-contributor.css';
15+
import template from './manage-users-view-contributor.pug';
16+
import utilitiesMixin from './utilities-mixin.js';
17+
18+
export class ManageUsersViewContributor extends utilitiesMixin(PolymerElement) {
19+
static get template() {
20+
return html([`
21+
<style>${css.toString()}</style>
22+
${template()}
23+
`]);
24+
}
25+
26+
/**
27+
* Object describing property-related metadata used by Polymer features
28+
*/
29+
static get properties() {
30+
return {
31+
user: {type: String, value: 'Loading...'},
32+
ownedNamespace: {type: Object, value: () => ({})},
33+
newContribEmail: String,
34+
contribError: Object,
35+
contributorInputEl: Object,
36+
};
37+
}
38+
/**
39+
* Main ready method for Polymer Elements.
40+
*/
41+
ready() {
42+
super.ready();
43+
this.contributorInputEl = this.$.ContribEmail;
44+
}
45+
46+
/**
47+
* Triggers an API call to create a new Contributor
48+
*/
49+
addNewContrib() {
50+
// Need to call the api directly here.
51+
const api = this.$.AddContribAjax;
52+
api.body = {contributor: this.newContribEmail};
53+
api.generateRequest();
54+
}
55+
/**
56+
* Triggers an API call to remove a Contributor
57+
* @param {Event} e
58+
*/
59+
removeContributor(e) {
60+
const api = this.$.RemoveContribAjax;
61+
api.body = {contributor: e.model.item};
62+
api.generateRequest();
63+
}
64+
/**
65+
* Takes an event from iron-ajax and isolates the error from a request that
66+
* failed
67+
* @param {IronAjaxEvent} e
68+
* @return {string}
69+
*/
70+
_isolateErrorFromIronRequest(e) {
71+
const bd = e.detail.request.response||{};
72+
return bd.error || e.detail.error || e.detail;
73+
}
74+
/**
75+
* Iron-Ajax response / error handler for addNewContributor
76+
* @param {IronAjaxEvent} e
77+
*/
78+
handleContribCreate(e) {
79+
if (e.detail.error) {
80+
const error = this._isolateErrorFromIronRequest(e);
81+
this.contribCreateError = error;
82+
return;
83+
}
84+
this.contributorList = e.detail.response;
85+
this.newContribEmail = this.contribCreateError = '';
86+
}
87+
/**
88+
* Iron-Ajax response / error handler for removeContributor
89+
* @param {IronAjaxEvent} e
90+
*/
91+
handleContribDelete(e) {
92+
if (e.detail.error) {
93+
const error = this._isolateErrorFromIronRequest(e);
94+
this.contribCreateError = error;
95+
return;
96+
}
97+
this.contributorList = e.detail.response;
98+
this.newContribEmail = this.contribCreateError = '';
99+
}
100+
/**
101+
* Iron-Ajax error handler for getContributors
102+
* @param {IronAjaxEvent} e
103+
*/
104+
onContribFetchError(e) {
105+
const error = this._isolateErrorFromIronRequest(e);
106+
this.contribError = error;
107+
this.$.ContribError.show();
108+
}
109+
}
110+
/* eslint-disable max-len */
111+
customElements.define('manage-users-view-contributor', ManageUsersViewContributor);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
iron-ajax#RemoveContribAjax(method='DELETE', url='/api/workgroup/remove-contributor/[[ownedNamespace.namespace]]',
2+
on-response='handleContribDelete', on-error='handleContribDelete', handle-as='json', content-type='application/json')
3+
iron-ajax#AddContribAjax(method='POST', url='/api/workgroup/add-contributor/[[ownedNamespace.namespace]]',
4+
on-response='handleContribCreate', on-error='handleContribCreate', handle-as='json', content-type='application/json')
5+
iron-ajax#GetContribsAjax(auto='[[!empty(ownedNamespace)]]', url='/api/workgroup/get-contributors/[[ownedNamespace.namespace]]',
6+
last-response='{{contributorList}}', on-error='onContribFetchError', handle-as='json')
7+
h2
8+
iron-icon.icon(icon='kubeflow:account-group')
9+
span.text
10+
| Contributors for -
11+
|
12+
code [[ownedNamespace.namespace]]
13+
.content.small
14+
md2-input(label='Email Addresses', value='{{newContribEmail}}', on-submit='addNewContrib', placeholder='Add by email address', error$='[[contribCreateError]]')
15+
.prefix(slot='prefix')
16+
template(is='dom-repeat', items='[[contributorList]]')
17+
paper-chip(on-remove='removeContributor') [[item]]
18+
paper-toast#ContribError(duration=5000)
19+
| Failed to fetch contributor list for {{ownedNamespace.namespace}}, because:
20+
strong [[contribError]]
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/* eslint-disable max-len */
2+
import '@polymer/test-fixture/test-fixture';
3+
import 'jasmine-ajax';
4+
import {mockIronAjax, yieldForRequests} from '../ajax_test_helper';
5+
import {flush} from '@polymer/polymer/lib/utils/flush.js';
6+
7+
import './dashboard-view';
8+
9+
const FIXTURE_ID = 'manage-users-view-contributor-fixture';
10+
const MU_VIEW_SELECTOR_ID = 'test-manage-users-contributor-view';
11+
const TEMPLATE = `
12+
<test-fixture id="${FIXTURE_ID}">
13+
<template>
14+
<manage-users-view-contributor id="${MU_VIEW_SELECTOR_ID}"></manage-users-view-contributor>
15+
</template>
16+
</test-fixture>
17+
`;
18+
const user = '[email protected]';
19+
const ownedNs = {namespace: 'ns1', role: 'owner'};
20+
21+
describe('Manage Users View Contributor', () => {
22+
let manageUsersViewContributor;
23+
24+
beforeAll(() => {
25+
jasmine.Ajax.install();
26+
const div = document.createElement('div');
27+
div.innerHTML = TEMPLATE;
28+
document.body.appendChild(div);
29+
});
30+
31+
beforeEach(() => {
32+
document.getElementById(FIXTURE_ID).create();
33+
manageUsersViewContributor = document.getElementById(MU_VIEW_SELECTOR_ID);
34+
});
35+
36+
afterEach(() => {
37+
document.getElementById(FIXTURE_ID).restore();
38+
});
39+
40+
afterAll(() => {
41+
jasmine.Ajax.uninstall();
42+
});
43+
44+
it('Should handle errors correctly', async () => {
45+
mockIronAjax(
46+
manageUsersViewContributor.$.GetContribsAjax,
47+
'Failed for test',
48+
true,
49+
);
50+
51+
manageUsersViewContributor.user = user;
52+
manageUsersViewContributor.ownedNamespace = ownedNs;
53+
54+
flush();
55+
await yieldForRequests();
56+
57+
expect(manageUsersViewContributor.$.ContribError.opened)
58+
.toBe(
59+
true,
60+
'Error toast is not opened'
61+
);
62+
expect(manageUsersViewContributor.contribError)
63+
.toBe('Failed for test');
64+
});
65+
66+
it('Should add contributors correctly', async () => {
67+
const contribList = ['[email protected]', '[email protected]'];
68+
const verificationContribs = ['[email protected]'];
69+
mockIronAjax(
70+
manageUsersViewContributor.$.GetContribsAjax,
71+
contribList,
72+
);
73+
mockIronAjax(
74+
manageUsersViewContributor.$.AddContribAjax,
75+
verificationContribs,
76+
);
77+
78+
manageUsersViewContributor.user = user;
79+
manageUsersViewContributor.ownedNamespace = ownedNs;
80+
81+
flush();
82+
await yieldForRequests();
83+
84+
const input = manageUsersViewContributor.shadowRoot.querySelector('md2-input');
85+
input.value = '[email protected]';
86+
input.fireEnter();
87+
88+
await yieldForRequests();
89+
90+
expect(manageUsersViewContributor.contributorList)
91+
.toEqual(
92+
verificationContribs,
93+
'Invalid list of contributors'
94+
);
95+
});
96+
97+
it('Should remove contributors correctly', async () => {
98+
const contribList = ['[email protected]', '[email protected]'];
99+
const verificationContribs = ['[email protected]'];
100+
mockIronAjax(
101+
manageUsersViewContributor.$.GetContribsAjax,
102+
contribList,
103+
);
104+
mockIronAjax(
105+
manageUsersViewContributor.$.RemoveContribAjax,
106+
verificationContribs,
107+
);
108+
109+
manageUsersViewContributor.user = user;
110+
manageUsersViewContributor.ownedNamespace = ownedNs;
111+
112+
flush();
113+
await yieldForRequests();
114+
115+
const chip = manageUsersViewContributor.shadowRoot.querySelector('md2-input paper-chip:nth-of-type(1)');
116+
chip.fireRemove({});
117+
118+
await yieldForRequests();
119+
120+
expect(manageUsersViewContributor.contributorList)
121+
.toEqual(
122+
verificationContribs,
123+
'Invalid list of contributors'
124+
);
125+
});
126+
127+
it('UI State should show contribs when namespace available', async () => {
128+
const contribList = ['[email protected]', '[email protected]'];
129+
mockIronAjax(
130+
manageUsersViewContributor.$.GetContribsAjax,
131+
contribList,
132+
);
133+
134+
manageUsersViewContributor.user = user;
135+
manageUsersViewContributor.ownedNamespace = ownedNs;
136+
137+
flush();
138+
await yieldForRequests();
139+
140+
expect(manageUsersViewContributor.shadowRoot.querySelector('h2 > .text').innerText)
141+
.toBe('Contributors for - ns1');
142+
143+
// View prop expectations
144+
expect(manageUsersViewContributor.contributorList)
145+
.toEqual(
146+
contribList,
147+
'Invalid list of contributors'
148+
);
149+
});
150+
});

0 commit comments

Comments
 (0)