From 6400f839fb377bf0bd684bb6baf1b1528b5ebf59 Mon Sep 17 00:00:00 2001 From: mslichao Date: Mon, 6 May 2019 11:21:46 +0800 Subject: [PATCH 1/7] New UI for user management --- src/webportal/config/webpack.common.js | 2 +- src/webportal/src/app/components/loading.jsx | 17 +- .../src/app/user/fabric/user-view.jsx | 38 ++ .../src/app/user/fabric/userView/Context.jsx | 21 + .../src/app/user/fabric/userView/Filter.jsx | 100 +++++ .../src/app/user/fabric/userView/Ordering.jsx | 51 +++ .../app/user/fabric/userView/Pagination.jsx | 44 +++ .../app/user/fabric/userView/Paginator.jsx | 149 +++++++ .../src/app/user/fabric/userView/Table.jsx | 163 ++++++++ .../src/app/user/fabric/userView/TopBar.jsx | 352 +++++++++++++++++ .../src/app/user/fabric/userView/index.jsx | 370 ++++++++++++++++++ .../userView}/user-edit-modal-component.ejs | 6 +- .../userView/user-edit-modal-component.scss | 58 +++ .../src/app/user/fabric/userView/utils.jsx | 24 ++ .../user/user-view/user-table.component.ejs | 1 - .../user/user-view/user-view.component.ejs | 13 - .../app/user/user-view/user-view.component.js | 285 -------------- .../user/user-view/user-view.component.scss | 68 ---- 18 files changed, 1387 insertions(+), 375 deletions(-) create mode 100644 src/webportal/src/app/user/fabric/user-view.jsx create mode 100644 src/webportal/src/app/user/fabric/userView/Context.jsx create mode 100644 src/webportal/src/app/user/fabric/userView/Filter.jsx create mode 100644 src/webportal/src/app/user/fabric/userView/Ordering.jsx create mode 100644 src/webportal/src/app/user/fabric/userView/Pagination.jsx create mode 100644 src/webportal/src/app/user/fabric/userView/Paginator.jsx create mode 100644 src/webportal/src/app/user/fabric/userView/Table.jsx create mode 100644 src/webportal/src/app/user/fabric/userView/TopBar.jsx create mode 100644 src/webportal/src/app/user/fabric/userView/index.jsx rename src/webportal/src/app/user/{user-view => fabric/userView}/user-edit-modal-component.ejs (91%) create mode 100644 src/webportal/src/app/user/fabric/userView/user-edit-modal-component.scss create mode 100644 src/webportal/src/app/user/fabric/userView/utils.jsx delete mode 100644 src/webportal/src/app/user/user-view/user-table.component.ejs delete mode 100644 src/webportal/src/app/user/user-view/user-view.component.ejs delete mode 100644 src/webportal/src/app/user/user-view/user-view.component.js delete mode 100644 src/webportal/src/app/user/user-view/user-view.component.scss diff --git a/src/webportal/config/webpack.common.js b/src/webportal/config/webpack.common.js index 8cec53e739..cecfa8f4e5 100644 --- a/src/webportal/config/webpack.common.js +++ b/src/webportal/config/webpack.common.js @@ -57,7 +57,7 @@ const config = (env, argv) => ({ 'home': './src/app/home/home.jsx', 'layout': './src/app/layout/layout.component.js', 'register': './src/app/user/user-register/user-register.component.js', - 'userView': './src/app/user/user-view/user-view.component.js', + 'userView': './src/app/user/fabric/user-view.jsx', 'batchRegister': './src/app/user/fabric/batch-register.jsx', 'changePassword': './src/app/user/change-password/change-password.component.js', 'dashboard': './src/app/dashboard/dashboard.component.js', diff --git a/src/webportal/src/app/components/loading.jsx b/src/webportal/src/app/components/loading.jsx index 42d3e7cd74..cca3752efd 100644 --- a/src/webportal/src/app/components/loading.jsx +++ b/src/webportal/src/app/components/loading.jsx @@ -20,6 +20,7 @@ import c from 'classnames'; import {isEqual, isNil} from 'lodash'; import {Spinner, SpinnerSize} from 'office-ui-fabric-react/lib/Spinner'; import React, {useLayoutEffect, useState} from 'react'; +import PropTypes from 'prop-types'; import t from './tachyons.scss'; @@ -35,7 +36,7 @@ export const Loading = () => ( // min-height issue hack // https://stackoverflow.com/questions/8468066/child-inside-parent-with-min-height-100-not-inheriting-height -export const SpinnerLoading = () => { +export const SpinnerLoading = ({label = 'Loading...'}) => { const [style, setStyle] = useState({}); useLayoutEffect(() => { function layout() { @@ -67,19 +68,27 @@ export const SpinnerLoading = () => {
-
Loading...
+
{label}
); }; -export const MaskSpinnerLoading = () => ( +SpinnerLoading.propTypes = { + label: PropTypes.string, +}; + +export const MaskSpinnerLoading = ({label = 'Loading...'}) => (
-
Loading...
+
{label}
); + +MaskSpinnerLoading.propTypes = { + label: PropTypes.string, +}; diff --git a/src/webportal/src/app/user/fabric/user-view.jsx b/src/webportal/src/app/user/fabric/user-view.jsx new file mode 100644 index 0000000000..0455fb7793 --- /dev/null +++ b/src/webportal/src/app/user/fabric/user-view.jsx @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import 'core-js/stable'; + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import UserView from './userView'; + +const contentWrapper = document.getElementById('content-wrapper'); + +ReactDOM.render(, contentWrapper); + +document.getElementById('sidebar-menu--cluster-view--user-management').classList.add('active'); + +function layout() { + setTimeout(function() { + contentWrapper.style.height = contentWrapper.style.minHeight; + }, 10); +} + +window.addEventListener('resize', layout); +window.addEventListener('load', layout); diff --git a/src/webportal/src/app/user/fabric/userView/Context.jsx b/src/webportal/src/app/user/fabric/userView/Context.jsx new file mode 100644 index 0000000000..87c60df37c --- /dev/null +++ b/src/webportal/src/app/user/fabric/userView/Context.jsx @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import React from 'react'; + +export default React.createContext({ +}); diff --git a/src/webportal/src/app/user/fabric/userView/Filter.jsx b/src/webportal/src/app/user/fabric/userView/Filter.jsx new file mode 100644 index 0000000000..24cdb44923 --- /dev/null +++ b/src/webportal/src/app/user/fabric/userView/Filter.jsx @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import {toBool} from './utils'; + +const LOCAL_STORAGE_KEY = 'pai-user-filter'; + +class Filter { + /** + * @param {Set?} admins + * @param {Set?} virtualClusters + */ + constructor( + keyword = '', + admins = new Set(), + virtualClusters = new Set(), + ) { + this.keyword = keyword; + this.admins = admins; + this.virtualClusters = virtualClusters; + } + + save() { + const content = JSON.stringify({ + admins: Array.from(this.admins), + virtualClusters: Array.from(this.virtualClusters), + }); + window.localStorage.setItem(LOCAL_STORAGE_KEY, content); + } + + load() { + try { + const content = window.localStorage.getItem(LOCAL_STORAGE_KEY); + const {admins, virtualClusters} = JSON.parse(content); + if (Array.isArray(admins)) { + this.admins = new Set(admins); + } + if (Array.isArray(virtualClusters)) { + this.virtualClusters = new Set(virtualClusters); + } + } catch (e) { + window.localStorage.removeItem(LOCAL_STORAGE_KEY); + } + } + + /** + * @param {any[]} users + */ + apply(users) { + const {keyword, admins, virtualClusters} = this; + + const filters = []; + if (keyword !== '') { + filters.push(({username, virtualCluster}) => ( + username.indexOf(keyword) > -1 || + virtualCluster.indexOf(keyword) > -1 + )); + } + if (admins.size > 0) { + filters.push((user) => admins.has(user.admin)); + } + if (virtualClusters.size > 0) { + filters.push(({virtualCluster, admin}) => { + if (toBool(admin)) { + return true; + } + if (virtualCluster) { + const vcs = virtualCluster.split(','); + for (let vc of virtualClusters) { + if (vcs.indexOf(vc) == -1) { + return false; + } + } + } else { + return false; + } + return true; + }); + } + if (filters.length === 0) return users; + + return users.filter((user) => filters.every((filter) => filter(user))); + } +} + +export default Filter; diff --git a/src/webportal/src/app/user/fabric/userView/Ordering.jsx b/src/webportal/src/app/user/fabric/userView/Ordering.jsx new file mode 100644 index 0000000000..1734db0f93 --- /dev/null +++ b/src/webportal/src/app/user/fabric/userView/Ordering.jsx @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import {toBool, getVirtualCluster} from './utils'; + +export default class Ordering { + /** + * @param {"username" | "admin" | "virtualCluster"} field + * @param {boolean | undefined} descending + */ + constructor(field, descending = false) { + this.field = field; + this.descending = descending; + } + + apply(users) { + const {field, descending} = this; + let comparator; + if (field == null) { + return users; + } + if (field === 'username') { + comparator = descending + ? (a, b) => (String(b.username).localeCompare(a.username)) + : (a, b) => (String(a.username).localeCompare(b.username)); + } else if (field === 'admin') { + comparator = descending + ? (a, b) => toBool(b.admin) - toBool(a.admin) + : (a, b) => toBool(a.admin) - toBool(b.admin); + } else if (field === 'virtualCluster') { + comparator = descending + ? (a, b) => String(getVirtualCluster(b)).localeCompare(getVirtualCluster(a)) + : (a, b) => String(getVirtualCluster(a)).localeCompare(getVirtualCluster(b)); + } + return users.slice().sort(comparator); + } +} diff --git a/src/webportal/src/app/user/fabric/userView/Pagination.jsx b/src/webportal/src/app/user/fabric/userView/Pagination.jsx new file mode 100644 index 0000000000..0789101249 --- /dev/null +++ b/src/webportal/src/app/user/fabric/userView/Pagination.jsx @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +export default class Pagination { + /** + * @param {number} itemsPerPage + * @param {number} pageIndex + */ + constructor( + itemsPerPage = 20, + pageIndex = 0, + ) { + this.itemsPerPage = itemsPerPage; + this.pageIndex = pageIndex; + } + + /** + * @param {any[]} items + * @returns {any[]} + */ + apply(items) { + const {itemsPerPage, pageIndex} = this; + const start = itemsPerPage * pageIndex; + const end = itemsPerPage * (pageIndex + 1); + return items.slice(start, end).map((item) => { + item.key = item.username; + return item; + }); + } +} diff --git a/src/webportal/src/app/user/fabric/userView/Paginator.jsx b/src/webportal/src/app/user/fabric/userView/Paginator.jsx new file mode 100644 index 0000000000..70da3b14f6 --- /dev/null +++ b/src/webportal/src/app/user/fabric/userView/Paginator.jsx @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import React, {useContext} from 'react'; + +import {CommandBar} from 'office-ui-fabric-react'; + +import Context from './Context'; +import Pagination from './Pagination'; + +export default function Paginator() { + const {filteredUsers, pagination, setPagination} = useContext(Context); + const {itemsPerPage, pageIndex} = pagination; + const length = filteredUsers !== null ? filteredUsers.length : 0; + const maxPageIndex = Math.floor(length / itemsPerPage); + const start = itemsPerPage * pageIndex + 1; + const end = Math.min(itemsPerPage * (pageIndex + 1), length); + + /** @type {import('office-ui-fabric-react').ICommandBarItemProps[]} */ + const farItems = []; + + /** @type {import('office-ui-fabric-react').IButtonStyles} */ + const buttonStyles = { + root: {backgroundColor: 'white'}, + rootDisabled: {backgroundColor: 'white'}, + }; + + function onClickItemsPerPage(event, {key}) { + setPagination(new Pagination(key)); + } + + farItems.push({ + key: 'itemsPerPage', + text: `${itemsPerPage} items per page`, + buttonStyles, + menuIconProps: {iconName: 'ChevronUp'}, + subMenuProps: { + items: [20, 50, 100].map((number) => ({ + key: String(number), + text: String(number), + onClick: onClickItemsPerPage, + })), + }, + }); + + farItems.push({ + key: 'range', + text: `${start}-${end} of ${length}`, + buttonStyles, + checked: true, + disabled: true, + }); + + /** + * @param {number} pageIndex + */ + function setPage(pageIndex) { + /** + * @param {React.MouseEvent + ); +} + +function KeywordSearchBox() { + const {filter, setFilter} = useContext(Context); + function onKeywordChange(keyword) { + const {admins, virtualClusters} = filter; + setFilter(new Filter(keyword, admins, virtualClusters)); + } + + /** @type {import('office-ui-fabric-react').IStyle} */ + const rootStyles = {backgroundColor: 'transparent', alignSelf: 'center', width: 220}; + return ( + + ); +} +/* eslint-enable react/prop-types */ + +function TopBar() { + const [active, setActive] = useState(true); + const {allUsers, refreshAllUsers, getSelectedUsers, filter, setFilter, addUser, importCSV, removeUsers} = useContext(Context); + + const {admins, virtualClusters} = useMemo(() => { + const admins = Object.create(null); + const virtualClusters = Object.create(null); + + if (allUsers !== null) { + allUsers.forEach(function(user) { + admins[String(user.admin)] = true; + if (user.virtualCluster) { + const vcs = user.virtualCluster.split(','); + vcs.forEach((vc) => { + virtualClusters[vc] = true; + }); + } + }); + } + + return {admins, virtualClusters}; + }, [allUsers]); + + /** + * @type {import('office-ui-fabric-react').ICommandBarItemProps} + */ + const btnAddUser = { + key: 'addUser', + name: 'Add User', + buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + iconProps: { + iconName: 'Add', + }, + onClick: addUser, + }; + + /** + * @type {import('office-ui-fabric-react').ICommandBarItemProps} + */ + const btnImportCSV = { + key: 'importCSV', + name: 'Import CSV', + buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + iconProps: { + iconName: 'Stack', + }, + onClick: importCSV, + }; + + /** + * @type {import('office-ui-fabric-react').ICommandBarItemProps} + */ + const btnRemove = { + key: 'remove', + name: 'Remove', + buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + iconProps: { + iconName: 'UserRemove', + }, + onClick: removeUsers, + }; + + /** + * @type {import('office-ui-fabric-react').ICommandBarItemProps} + */ + const btnRefresh = { + key: 'refresh', + name: 'Refresh', + buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + iconProps: { + iconName: 'Refresh', + }, + onClick: refreshAllUsers, + }; + + /** + * @type {import('office-ui-fabric-react').ICommandBarItemProps} + */ + const inputKeyword = { + key: 'keyword', + commandBarButtonAs: KeywordSearchBox, + }; + + /** + * @type {import('office-ui-fabric-react').ICommandBarItemProps} + */ + const btnFilters = { + key: 'filters', + name: 'Filters', + iconProps: {iconName: 'Filter'}, + menuIconProps: {iconName: active ? 'ChevronUp' : 'ChevronDown'}, + onClick() { + setActive(!active); + }, + onRender(item) { + return ( + + Filters + + ); + }, + }; + + /** + * @type {import('office-ui-fabric-react').ICommandBarItemProps} + */ + const btnClear = { + key: 'clear', + name: 'Clear', + buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + iconOnly: true, + iconProps: { + iconName: 'Cancel', + }, + onClick() { + setFilter(new Filter()); + setActive(false); + }, + }; + + /** + * @returns {import('office-ui-fabric-react').ICommandBarItemProps} + */ + function getBtnAdmin() { + /** + * @param {React.SyntheticEvent} event + * @param {import('office-ui-fabric-react').IContextualMenuItem} item + */ + function onClick(event, {key, checked}) { + event.preventDefault(); + const {keyword, virtualClusters} = filter; + const admins = new Set(filter.admins); + if (checked) { + admins.delete(key); + } else { + admins.add(key); + } + setFilter(new Filter(keyword, admins, virtualClusters)); + } + + /** + * @param {React.SyntheticEvent} event + */ + function onClearClick(event) { + event.preventDefault(); + const {keyword, virtualClusters} = filter; + setFilter(new Filter(keyword, new Set(), virtualClusters)); + } + + /** + * @param {string} key + * @param {string} text + * @returns {import('office-ui-fabric-react').IContextualMenuItem} + */ + function getItem(key) { + return { + key, + text: toBool(key) ? 'Yes' : 'No', + canCheck: true, + checked: filter.admins.has(key), + onClick: onClick, + }; + } + + return { + key: 'admin', + text: 'Admin', + buttonStyles: {root: {backgroundColor: 'transparent'}}, + iconProps: { + iconName: 'Clock', + }, + subMenuProps: { + items: Object.keys(admins).map(getItem).concat([{ + key: 'divider', + itemType: ContextualMenuItemType.Divider, + }, + { + key: 'clear', + text: 'Clear', + onClick: onClearClick, + }, + ]), + }, + commandBarButtonAs: FilterButton, + }; + } + + /** + * @returns {import('office-ui-fabric-react').ICommandBarItemProps} + */ + function getBtnVirtualCluster() { + /** + * @param {React.SyntheticEvent} event + * @param {import('office-ui-fabric-react').IContextualMenuItem} item + */ + function onClick(event, {key, checked}) { + event.preventDefault(); + const {keyword, admins} = filter; + const virtualClusters = new Set(filter.virtualClusters); + if (checked) { + virtualClusters.delete(key); + } else { + virtualClusters.add(key); + } + setFilter(new Filter(keyword, admins, virtualClusters)); + } + + /** + * @param {React.SyntheticEvent} event + */ + function onClearClick(event) { + event.preventDefault(); + const {keyword, admins} = filter; + setFilter(new Filter(keyword, admins, new Set())); + } + + /** + * @param {string} key + * @param {string} text + * @returns {import('office-ui-fabric-react').IContextualMenuItem} + */ + function getItem(key) { + return { + key, + text: key, + canCheck: true, + checked: filter.virtualClusters.has(key), + onClick: onClick, + }; + } + + return { + key: 'virtualCluster', + name: 'Virtual Cluster', + buttonStyles: {root: {backgroundColor: 'transparent'}}, + iconProps: { + iconName: 'CellPhone', + }, + subMenuProps: { + items: Object.keys(virtualClusters).map(getItem).concat([{ + key: 'divider', + itemType: ContextualMenuItemType.Divider, + }, + { + key: 'clear', + text: 'Clear', + onClick: onClearClick, + }, + ]), + }, + commandBarButtonAs: FilterButton, + }; + } + + const topBarItems = [btnAddUser, btnImportCSV]; + const selectedUsers = getSelectedUsers(); + if (selectedUsers.length > 0 && findIndex(selectedUsers, (user) => toBool(user.admin)) == -1) { + topBarItems.push(btnRemove); + } + topBarItems.push(btnRefresh); + const topBarFarItems = [btnFilters]; + + const filterBarItems = [inputKeyword]; + const filterBarFarItems = [ + getBtnVirtualCluster(), + getBtnAdmin(), + btnClear, + ]; + + return ( + + + {active ? : null} + + ); +} + +export default TopBar; diff --git a/src/webportal/src/app/user/fabric/userView/index.jsx b/src/webportal/src/app/user/fabric/userView/index.jsx new file mode 100644 index 0000000000..655e385e6d --- /dev/null +++ b/src/webportal/src/app/user/fabric/userView/index.jsx @@ -0,0 +1,370 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import React, {useState, useEffect, useMemo, useRef} from 'react'; + +import {initializeIcons} from 'office-ui-fabric-react'; +import {Fabric, Stack} from 'office-ui-fabric-react'; +import {debounce} from 'lodash'; + +import {MaskSpinnerLoading} from '../../../components/loading'; +import MessageBox from '../components/MessageBox'; +import webportalConfig from '../../../config/webportal.config'; +import userAuth from '../../user-auth/user-auth.component'; + +import Context from './Context'; +import TopBar from './TopBar'; +import Table from './Table'; +import Ordering from './Ordering'; +import Filter from './Filter'; +import Pagination from './Pagination'; +import Paginator from './Paginator'; + +require('bootstrap/js/modal.js'); +const userEditModalComponent = require('./user-edit-modal-component.ejs'); +require('./user-edit-modal-component.scss'); + +initializeIcons(); + +export default function UserView() { + const [loading, setLoading] = useState({'show': false, 'text': ''}); + const showLoading = (text) => { + setLoading({'show': true, 'text': text}); + }; + const hideLoading = () => { + setLoading({'show': false}); + }; + + const [messageBox, setMessageBox] = useState({text: '', confirm: false, dismissedCallback: undefined, okCallback: undefined, cancelCallback: undefined}); + const showMessageBox = (value) => { + if (value == undefined || value == null) { + setMessageBox({text: ''}); + } else if (typeof value === 'string') { + setMessageBox({text: value}); + } else { + setMessageBox(value); + } + }; + const hideMessageBox = () => { + if (messageBox.dismissedCallback) { + messageBox.dismissedCallback(); + } + setMessageBox({text: ''}); + }; + + const [allUsers, setAllUsers] = useState([]); + const refreshAllUsers = () => { + userAuth.checkToken((token) => { + $.ajax({ + url: `${webportalConfig.restServerUri}/api/v1/user`, + type: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + dataType: 'json', + success: (data) => { + setAllUsers(data); + }, + error: (xhr) => { + const res = JSON.parse(xhr.responseText); + showMessageBox({ + text: res.message, + dismissedCallback: () => { + window.location.href = '/'; + }, + }); + }, + }); + }); + }; + useEffect(refreshAllUsers, []); + + const initialFilter = useMemo(() => { + const filter = new Filter(); + filter.load(); + return filter; + }); + const [filter, setFilter] = useState(initialFilter); + useEffect(() => filter.save(), [filter]); + + const [filteredUsers, setFilteredUsers] = useState(null); + const {current: applyFilter} = useRef(debounce((allUsers, /** @type {Filter} */filter) => { + setFilteredUsers(filter.apply(allUsers || [])); + }, 200)); + useEffect(() => { + applyFilter(allUsers, filter); + }, [applyFilter, allUsers, filter]); + + const [pagination, setPagination] = useState(new Pagination()); + useEffect(() => { + setPagination(new Pagination(pagination.itemsPerPage, 0)); + }, [filteredUsers]); + + const [selectedUsers, setSelectedUsers] = useState([]); + const [allSelected, setAllSelected] = useState(false); + const getSelectedUsers = () => { + if (allSelected) { + return pagination.apply(ordering.apply(filteredUsers || [])); + } else { + return selectedUsers; + } + }; + + const [ordering, setOrdering] = useState(new Ordering()); + + const addUser = () => { + window.location.href = '/register.html'; + }; + + const importCSV = () => { + window.location.href = '/batch-register.html'; + }; + + const removeUser = (user) => { + const token = userAuth.checkToken(); + return $.ajax({ + url: `${webportalConfig.restServerUri}/api/v1/user`, + data: { + username: user.username, + }, + type: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + dataType: 'json', + }); + }; + + const removeUserRecursively = (selected, index) => { + if (index == 0) { + showLoading('Processing...'); + } + if (index >= selected.length) { + hideLoading(); + setTimeout(() => { + showMessageBox({ + text: `Remove ${selected.length == 1 ? 'user' : 'users'} successfully.`, + dismissedCallback: () => { + setAllUsers([]); + refreshAllUsers(); + }, + }); + }, 100); + } else { + const user = selected[index]; + removeUser(user).then(() => { + removeUserRecursively(selected, ++index); + }, (xhr) => { + hideLoading(); + setTimeout(() => { + const res = JSON.parse(xhr.responseText); + showMessageBox({ + text: res.message, + dismissedCallback: () => { + setAllUsers([]); + refreshAllUsers(); + }, + }); + }, 100); + }); + } + }; + + const removeUsers = () => { + const selected = getSelectedUsers(); + showMessageBox({ + text: `Are you sure to remove ${selected.length == 1 ? 'the user' : 'these users'}?`, + confirm: true, + okCallback: () => { + removeUserRecursively(selected, 0); + }, + }); + }; + + const editUser = (user) => { + showEditInfo(user.username, user.admin, user.virtualCluster, user.hasGithubPAT); + }; + + const showEditInfo = (username, isAdmin, vcList, hasGithubPAT) => { + $('#modalPlaceHolder').html(userEditModalComponent({ + 'username': username, + 'isAdmin': String(isAdmin), + 'vcList': vcList, + 'hasGithubPAT': String(hasGithubPAT), + updateUserVc, + updateUserAccount, + updateUserGithubPAT, + })); + $('#userEditModal').modal('show'); + }; + + const updateUserVc = (username) => { + const virtualCluster = $('#form-update-virtual-cluster :input[name=virtualCluster]').val(); + userAuth.checkToken((token) => { + $.ajax({ + url: `${webportalConfig.restServerUri}/api/v1/user/${username}/virtualClusters`, + data: { + virtualClusters: virtualCluster, + }, + type: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + }, + dataType: 'json', + success: (data) => { + if (data.error) { + showMessageBox(data.message); + } else { + showMessageBox({ + text: 'Update user information successfully', + dismissedCallback: () => { + $('#userEditModal').modal('hide'); + setAllUsers([]); + refreshAllUsers(); + }, + }); + } + }, + error: (xhr, textStatus, error) => { + $('#form-update-virtual-cluster').trigger('reset'); + const res = JSON.parse(xhr.responseText); + showMessageBox(res.message); + }, + }); + }); + }; + + const updateUserAccount = (username) => { + const password = $('#form-update-account :input[name=password]').val(); + const admin = $('#form-update-account :input[name=admin]').is(':checked') ? true : false; + userAuth.checkToken((token) => { + $.ajax({ + url: `${webportalConfig.restServerUri}/api/v1/user`, + data: { + username, + password, + admin: admin, + modify: true, + }, + type: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + }, + dataType: 'json', + success: (data) => { + if (data.error) { + showMessageBox(data.message); + } else { + showMessageBox({ + text: 'Update user basic information successfully', + dismissedCallback: () => { + $('#userEditModal').modal('hide'); + setAllUsers([]); + refreshAllUsers(); + }, + }); + } + }, + error: (xhr, textStatus, error) => { + $('#form-update-account').trigger('reset'); + const res = JSON.parse(xhr.responseText); + showMessageBox(res.message); + }, + }); + }); + }; + + const updateUserGithubPAT = (username) => { + const githubPAT = $('#form-update-github-token :input[name=githubPAT]').val(); + userAuth.checkToken((token) => { + $.ajax({ + url: `${webportalConfig.restServerUri}/api/v1/user/${username}/githubPAT`, + data: { + githubPAT: githubPAT, + }, + type: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + }, + dataType: 'json', + success: (data) => { + if (data.error) { + showMessageBox(data.message); + } else { + showMessageBox({ + text: 'Update user information successfully', + dismissedCallback: () => { + $('#userEditModal').modal('hide'); + setAllUsers([]); + refreshAllUsers(); + }, + }); + } + }, + error: (xhr, textStatus, error) => { + $('#form-update-github-token').trigger('reset'); + const res = JSON.parse(xhr.responseText); + showMessageBox(res.message); + }, + }); + }); + }; + + window.updateUserVc = updateUserVc; + window.updateUserAccount = updateUserAccount; + window.updateUserGithubPAT = updateUserGithubPAT; + + const context = { + allUsers, + refreshAllUsers, + filteredUsers, + ordering, + setOrdering, + filter, + setFilter, + pagination, + setPagination, + setSelectedUsers, + getSelectedUsers, + setAllSelected, + addUser, + importCSV, + removeUsers, + editUser, + }; + + return ( + + + + + + + + + + + + + + + {loading.show && } + {messageBox.text && } +
+ + ); +} diff --git a/src/webportal/src/app/user/user-view/user-edit-modal-component.ejs b/src/webportal/src/app/user/fabric/userView/user-edit-modal-component.ejs similarity index 91% rename from src/webportal/src/app/user/user-view/user-edit-modal-component.ejs rename to src/webportal/src/app/user/fabric/userView/user-edit-modal-component.ejs index 1081eb82dd..8568358af4 100644 --- a/src/webportal/src/app/user/user-view/user-edit-modal-component.ejs +++ b/src/webportal/src/app/user/fabric/userView/user-edit-modal-component.ejs @@ -9,7 +9,7 @@

Change Userinfo

-
+
@@ -26,7 +26,7 @@

Update Virtual Clusters

- + > @@ -36,7 +36,7 @@

Update Github PAT

- + { + return (String(val)).toLowerCase() === 'true' || (String(val)).toLowerCase() === 'yes'; +}; + +export const getVirtualCluster = (user) => { + return toBool(user.admin) ? 'All virtual clusters' : user.virtualCluster; +}; diff --git a/src/webportal/src/app/user/user-view/user-table.component.ejs b/src/webportal/src/app/user/user-view/user-table.component.ejs deleted file mode 100644 index 88eff07a9c..0000000000 --- a/src/webportal/src/app/user/user-view/user-table.component.ejs +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/src/webportal/src/app/user/user-view/user-view.component.ejs b/src/webportal/src/app/user/user-view/user-view.component.ejs deleted file mode 100644 index 41a6f0d745..0000000000 --- a/src/webportal/src/app/user/user-view/user-view.component.ejs +++ /dev/null @@ -1,13 +0,0 @@ -
<%= breadcrumb({ breadcrumbTitle: "User Management" }) %>
- -
-
-
- - -
- <%= userTable({ userLists: [] }) %> -
-
- <%= loading() %> -
diff --git a/src/webportal/src/app/user/user-view/user-view.component.js b/src/webportal/src/app/user/user-view/user-view.component.js deleted file mode 100644 index 3be842aa6a..0000000000 --- a/src/webportal/src/app/user/user-view/user-view.component.js +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -// module dependencies - -require('bootstrap/js/modal.js'); -require('datatables.net/js/jquery.dataTables.js'); -require('datatables.net-bs/js/dataTables.bootstrap.js'); -require('datatables.net-bs/css/dataTables.bootstrap.css'); -require('datatables.net-plugins/sorting/natural.js'); -require('datatables.net-plugins/sorting/title-numeric.js'); -require('./user-view.component.scss'); - -const breadcrumbComponent = require('../../job/breadcrumb/breadcrumb.component.ejs'); -const loadingComponent = require('../../job/loading/loading.component.ejs'); -const userViewComponent = require('./user-view.component.ejs'); -const userTableComponent = require('./user-table.component.ejs'); -const userEditModalComponent = require('./user-edit-modal-component.ejs'); -const loading = require('../../job/loading/loading.component'); -const webportalConfig = require('../../config/webportal.config.js'); -const userAuth = require('../user-auth/user-auth.component'); - -let table = null; - -const userViewHtml = userViewComponent({ - breadcrumb: breadcrumbComponent, - loading: loadingComponent, - userTable: userTableComponent, -}); - -const removeUser = (username) => { - const res = confirm('Are you sure to remove the user?'); - if (res) { - userAuth.checkToken((token) => { - $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user`, - data: { - username, - }, - type: 'DELETE', - headers: { - Authorization: `Bearer ${token}`, - }, - dataType: 'json', - success: (data) => { - if (data.error) { - alert(data.message); - } else { - alert('Remove user successfully'); - } - window.location.href = '/user-view.html'; - }, - error: (xhr, textStatus, error) => { - const res = JSON.parse(xhr.responseText); - alert(res.message); - }, - }); - }); - } -}; - -const redirectToAddUser = () => { - window.location.href = '/register.html'; -}; - -const redirectToBatchAddUser = () => { - window.location.href = '/batch-register.html'; -}; - -const loadUsers = (limit, specifiedVc) => { - loading.showLoading(); - userAuth.checkToken((token) => { - $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user`, - type: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, - success: (data) => { - if (data.error) { - alert(data.message); - } else { - let displayDataSet = []; - let rowCount = Math.min(data.length, (limit && (/^\+?[0-9][\d]*$/.test(limit))) ? limit : 2000); - - for (let i = 0; i < rowCount; i++) { - let removeBtnStyle = - (data[i].admin === 'true') ? - '' : - ''; - displayDataSet.push({ - userName: data[i].username, - admin: (data[i].admin === 'true') ? 'Yes' : 'No', - vcName: (data[i].admin === 'true') ? 'All virtual clusters' : data[i].virtualCluster, - githubPAT: data[i].githubPAT, - edit: '', - remove: removeBtnStyle, - }); - } - $('#view-table').html(userTableComponent({})); - table = $('#user-table').dataTable({ - 'data': displayDataSet, - 'columns': [ - {title: 'User Name', data: 'userName'}, - {title: 'Admin', data: 'admin'}, - {title: 'Virtual Cluster List', data: 'vcName'}, - {title: 'Edit', data: 'edit'}, - {title: 'Remove', data: 'remove'}, - ], - 'scrollY': (($(window).height() - 265)) + 'px', - 'lengthMenu': [[20, 50, 100, -1], [20, 50, 100, 'All']], - 'columnDefs': [ - {type: 'natural', targets: [0, 1, 2, 3, 4]}, - ], - 'deferRender': true, - 'autoWidth': false, - }).api(); - } - resizeContentWrapper(); - loading.hideLoading(); - }, - error: (xhr, textStatus, error) => { - const res = JSON.parse(xhr.responseText); - alert(res.message); - resizeContentWrapper(); - loading.hideLoading(); - }, - }); - }); -}; - - -const showEditInfo = (username, isAdmin, vcList, hasGithubPAT) => { - $('#modalPlaceHolder').html(userEditModalComponent({ - 'username': username, - 'isAdmin': isAdmin, - 'vcList': vcList, - 'hasGithubPAT': hasGithubPAT, - updateUserVc, - updateUserAccount, - updateUserGithubPAT, - })); - $('#userEditModal').modal('show'); -}; - -const updateUserVc = (username) => { - const virtualCluster = $('#form-update-virtual-cluster :input[name=virtualCluster]').val(); - userAuth.checkToken((token) => { - $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user/${username}/virtualClusters`, - data: { - virtualClusters: virtualCluster, - }, - type: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, - dataType: 'json', - success: (data) => { - if (data.error) { - alert(data.message); - } else { - alert('Update user information successfully'); - } - window.location.href = '/user-view.html'; - }, - error: (xhr, textStatus, error) => { - $('#form-update-virtual-cluster').trigger('reset'); - const res = JSON.parse(xhr.responseText); - alert(res.message); - }, - }); - }); -}; - -const updateUserAccount = (username) => { - const password = $('#form-update-account :input[name=password]').val(); - const admin = $('#form-update-account :input[name=admin]').is(':checked') ? true : false; - userAuth.checkToken((token) => { - $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user`, - data: { - username, - password, - admin: admin, - modify: true, - }, - type: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, - dataType: 'json', - success: (data) => { - if (data.error) { - alert(data.message); - } else { - alert('Update user basic information successfully'); - window.location.href = '/user-view.html'; - } - }, - error: (xhr, textStatus, error) => { - $('#form-update-account').trigger('reset'); - const res = JSON.parse(xhr.responseText); - alert(res.message); - }, - }); - }); -}; - -const updateUserGithubPAT = (username) => { - const githubPAT = $('#form-update-github-token :input[name=githubPAT]').val(); - userAuth.checkToken((token) => { - $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user/${username}/githubPAT`, - data: { - githubPAT: githubPAT, - }, - type: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, - dataType: 'json', - success: (data) => { - if (data.error) { - alert(data.message); - } else { - alert('Update user information successfully'); - } - window.location.href = '/user-view.html'; - }, - error: (xhr, textStatus, error) => { - $('#form-update-github-token').trigger('reset'); - const res = JSON.parse(xhr.responseText); - alert(res.message); - }, - }); - }); -}; - -window.loadUsers = loadUsers; -window.removeUser = removeUser; -window.redirectToAddUser = redirectToAddUser; -window.redirectToBatchAddUser = redirectToBatchAddUser; -window.showEditInfo = showEditInfo; -window.updateUserVc = updateUserVc; -window.updateUserAccount = updateUserAccount; -window.updateUserGithubPAT = updateUserGithubPAT; - -const resizeContentWrapper = () => { - $('#content-wrapper').css({'height': $(window).height() + 'px'}); - if (table != null) { - $('.dataTables_scrollBody').css('height', (($(window).height() - 315)) + 'px'); - table.columns.adjust().draw(); - } -}; - -$('#content-wrapper').html(userViewHtml); - -$(document).ready(() => { - window.onresize = function(event) { - resizeContentWrapper(); - }; - $('#sidebar-menu--cluster-view--user-management').addClass('active'); - loadUsers(); - $('#content-wrapper').css({'overflow': 'hidden'}); -}); - -module.exports = {loadUsers, removeUser, showEditInfo, redirectToAddUser, updateUserVc, updateUserAccount, updateUserGithubPAT}; diff --git a/src/webportal/src/app/user/user-view/user-view.component.scss b/src/webportal/src/app/user/user-view/user-view.component.scss deleted file mode 100644 index 2a156d42c9..0000000000 --- a/src/webportal/src/app/user/user-view/user-view.component.scss +++ /dev/null @@ -1,68 +0,0 @@ -.divider { - border-top: 1px solid #7c7c7c; -} - -.table > tbody > tr > td { - vertical-align: middle; -} - -.disabled { - pointer-events: none; - color: silver; - cursor: default; -} - -.add-user-btn { - margin: 10px; - margin-top: 0px; - padding-left: 40px; - padding-right: 40px; -} - - -.form-register { - max-width: 360px; - padding: 30px; - margin: 0 auto; -} - -.form-register .form-register-heading, -.form-register .checkbox { - margin-bottom: 10px; -} - -.form-register .checkbox { - font-weight: normal; -} - -.form-register .form-control { - position: relative; - height: auto; - padding: 10px; - font-size: 16px; -} - -.form-register .form-control:focus { - z-index: 2; -} - -.form-register input[type="password"] { - margin-bottom: -1px; - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.form-register input[id="update-virtual-cluster-input-virtualCluster"] { - margin-bottom: 10px; - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.user-management-nav{ - border-bottom-color: #d2d6de !important; - border-right-color: #d2d6de !important; -} - -.user-edit-border { - padding:15px; -} From 214136a7161e5c8353220e5ace187596b73e83fd Mon Sep 17 00:00:00 2001 From: mslichao Date: Tue, 7 May 2019 16:04:46 +0800 Subject: [PATCH 2/7] refine code --- src/webportal/.eslintrc.js | 10 +- .../app/user/fabric/components/MessageBox.jsx | 2 +- src/webportal/src/app/user/fabric/conn.js | 98 ++++++++ .../src/app/user/fabric/user-view.jsx | 2 + .../src/app/user/fabric/userView/index.jsx | 235 +++++------------- .../fabric/userView/{utils.jsx => utils.js} | 0 6 files changed, 174 insertions(+), 173 deletions(-) create mode 100644 src/webportal/src/app/user/fabric/conn.js rename src/webportal/src/app/user/fabric/userView/{utils.jsx => utils.js} (100%) diff --git a/src/webportal/.eslintrc.js b/src/webportal/.eslintrc.js index b5d043fb14..56c3e5ed8a 100644 --- a/src/webportal/.eslintrc.js +++ b/src/webportal/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { "jquery": true, }, "extends": [ - "eslint:recommended", + "eslint:recommended", "plugin:react/recommended", "google" ], @@ -26,7 +26,13 @@ module.exports = { }, "overrides": [ { - "files": ["**/*.jsx", "src/app/job/job-view/fabric/**/*.js", "src/app/components/**/*.js", "src/app/home/**/*.js"], + "files": [ + "**/*.jsx", + "src/app/job/job-view/fabric/**/*.js", + "src/app/components/**/*.js", + "src/app/home/**/*.js", + "src/app/user/fabric/**/*.js", + ], "parser": "babel-eslint" } ] diff --git a/src/webportal/src/app/user/fabric/components/MessageBox.jsx b/src/webportal/src/app/user/fabric/components/MessageBox.jsx index 7c51f37dbb..4d772312ac 100644 --- a/src/webportal/src/app/user/fabric/components/MessageBox.jsx +++ b/src/webportal/src/app/user/fabric/components/MessageBox.jsx @@ -53,7 +53,7 @@ function MessageBox(props) { } > - {text} + {text} diff --git a/src/webportal/src/app/user/fabric/conn.js b/src/webportal/src/app/user/fabric/conn.js new file mode 100644 index 0000000000..371e60efcb --- /dev/null +++ b/src/webportal/src/app/user/fabric/conn.js @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import config from '../../config/webportal.config'; +import {checkToken} from '../user-auth/user-auth.component'; + +const fetchWrapper = async (...args) => { + const res = await fetch(...args); + const json = await res.json(); + if (res.ok) { + return json; + } else { + throw new Error(json.message); + } +}; + +export const getAllUsersRequest = async () => { + const url = `${config.restServerUri}/api/v1/user`; + const token = checkToken(); + return await fetchWrapper(url, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); +}; + +export const removeUserRequest = async (username) => { + const url = `${config.restServerUri}/api/v1/user`; + const token = checkToken(); + return await fetchWrapper(url, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + username: username, + }), + }); +}; + +export const updateUserVcRequest = async (username, virtualCluster) => { + const url = `${config.restServerUri}/api/v1/user/${username}/virtualClusters`; + const token = checkToken(); + return await fetchWrapper(url, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + virtualClusters: virtualCluster, + }), + }); +}; + +export const updateUserAccountRequest = async (username, password, admin) => { + const url = `${config.restServerUri}/api/v1/user`; + const token = checkToken(); + return await fetchWrapper(url, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + username, + password, + admin: admin, + modify: true, + }), + }); +}; + +export const updateUserGithubPATRequest = async (username, githubPAT) => { + const url = `${config.restServerUri}/api/v1/user/${username}/githubPAT`; + const token = checkToken(); + return await fetchWrapper(url, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + githubPAT: githubPAT, + }), + }); +}; diff --git a/src/webportal/src/app/user/fabric/user-view.jsx b/src/webportal/src/app/user/fabric/user-view.jsx index 0455fb7793..71d28b8615 100644 --- a/src/webportal/src/app/user/fabric/user-view.jsx +++ b/src/webportal/src/app/user/fabric/user-view.jsx @@ -16,6 +16,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import 'core-js/stable'; +import 'regenerator-runtime/runtime'; +import 'whatwg-fetch'; import React from 'react'; import ReactDOM from 'react-dom'; diff --git a/src/webportal/src/app/user/fabric/userView/index.jsx b/src/webportal/src/app/user/fabric/userView/index.jsx index 655e385e6d..8831c126c7 100644 --- a/src/webportal/src/app/user/fabric/userView/index.jsx +++ b/src/webportal/src/app/user/fabric/userView/index.jsx @@ -17,14 +17,11 @@ import React, {useState, useEffect, useMemo, useRef} from 'react'; -import {initializeIcons} from 'office-ui-fabric-react'; -import {Fabric, Stack} from 'office-ui-fabric-react'; +import {Fabric, Stack, initializeIcons} from 'office-ui-fabric-react'; import {debounce} from 'lodash'; import {MaskSpinnerLoading} from '../../../components/loading'; import MessageBox from '../components/MessageBox'; -import webportalConfig from '../../../config/webportal.config'; -import userAuth from '../../user-auth/user-auth.component'; import Context from './Context'; import TopBar from './TopBar'; @@ -33,6 +30,7 @@ import Ordering from './Ordering'; import Filter from './Filter'; import Pagination from './Pagination'; import Paginator from './Paginator'; +import {getAllUsersRequest, removeUserRequest, updateUserVcRequest, updateUserAccountRequest, updateUserGithubPATRequest} from '../conn'; require('bootstrap/js/modal.js'); const userEditModalComponent = require('./user-edit-modal-component.ejs'); @@ -55,6 +53,8 @@ export default function UserView() { setMessageBox({text: ''}); } else if (typeof value === 'string') { setMessageBox({text: value}); + } else if (!value.hasOwnProperty('text')) { + setMessageBox({text: String(value)}); } else { setMessageBox(value); } @@ -68,25 +68,13 @@ export default function UserView() { const [allUsers, setAllUsers] = useState([]); const refreshAllUsers = () => { - userAuth.checkToken((token) => { - $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user`, - type: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, - dataType: 'json', - success: (data) => { - setAllUsers(data); - }, - error: (xhr) => { - const res = JSON.parse(xhr.responseText); - showMessageBox({ - text: res.message, - dismissedCallback: () => { - window.location.href = '/'; - }, - }); + getAllUsersRequest().then((data) => { + setAllUsers(data); + }).catch((err) => { + showMessageBox({ + text: String(err), + dismissedCallback: () => { + window.location.href = '/'; }, }); }); @@ -134,63 +122,36 @@ export default function UserView() { window.location.href = '/batch-register.html'; }; - const removeUser = (user) => { - const token = userAuth.checkToken(); - return $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user`, - data: { - username: user.username, - }, - type: 'DELETE', - headers: { - Authorization: `Bearer ${token}`, - }, - dataType: 'json', - }); - }; - - const removeUserRecursively = (selected, index) => { - if (index == 0) { - showLoading('Processing...'); - } - if (index >= selected.length) { - hideLoading(); - setTimeout(() => { - showMessageBox({ - text: `Remove ${selected.length == 1 ? 'user' : 'users'} successfully.`, - dismissedCallback: () => { - setAllUsers([]); - refreshAllUsers(); - }, - }); - }, 100); - } else { - const user = selected[index]; - removeUser(user).then(() => { - removeUserRecursively(selected, ++index); - }, (xhr) => { - hideLoading(); - setTimeout(() => { - const res = JSON.parse(xhr.responseText); - showMessageBox({ - text: res.message, - dismissedCallback: () => { - setAllUsers([]); - refreshAllUsers(); - }, - }); - }, 100); - }); - } - }; - const removeUsers = () => { const selected = getSelectedUsers(); showMessageBox({ text: `Are you sure to remove ${selected.length == 1 ? 'the user' : 'these users'}?`, confirm: true, okCallback: () => { - removeUserRecursively(selected, 0); + showLoading('Processing...'); + Promise.all(selected.map((user) => removeUserRequest(user.username).catch((err) => err))) + .then((results) => { + hideLoading(); + const errors = results.filter((result) => result instanceof Error); + let message = `Remove ${selected.length == 1 ? 'user' : 'users'} `; + if (errors.length == 0) { + message += 'successfully.'; + } else { + message += `with ${errors.length} failed.`; + errors.forEach((error) => { + message += `\n${String(error)}`; + }); + } + setTimeout(() => { + showMessageBox({ + text: message, + dismissedCallback: () => { + setAllUsers([]); + refreshAllUsers(); + }, + }); + }, 100); + }); }, }); }; @@ -212,116 +173,50 @@ export default function UserView() { $('#userEditModal').modal('show'); }; + const updateUserInfoCallback = (data) => { + if (data.error) { + showMessageBox(data.message); + } else { + showMessageBox({ + text: 'Update user information successfully', + dismissedCallback: () => { + $('#userEditModal').modal('hide'); + setAllUsers([]); + refreshAllUsers(); + }, + }); + } + }; + const updateUserVc = (username) => { const virtualCluster = $('#form-update-virtual-cluster :input[name=virtualCluster]').val(); - userAuth.checkToken((token) => { - $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user/${username}/virtualClusters`, - data: { - virtualClusters: virtualCluster, - }, - type: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, - dataType: 'json', - success: (data) => { - if (data.error) { - showMessageBox(data.message); - } else { - showMessageBox({ - text: 'Update user information successfully', - dismissedCallback: () => { - $('#userEditModal').modal('hide'); - setAllUsers([]); - refreshAllUsers(); - }, - }); - } - }, - error: (xhr, textStatus, error) => { - $('#form-update-virtual-cluster').trigger('reset'); - const res = JSON.parse(xhr.responseText); - showMessageBox(res.message); - }, + updateUserVcRequest(username, virtualCluster) + .then(updateUserInfoCallback) + .catch((err) => { + $('#form-update-virtual-cluster').trigger('reset'); + showMessageBox(err); }); - }); }; const updateUserAccount = (username) => { const password = $('#form-update-account :input[name=password]').val(); const admin = $('#form-update-account :input[name=admin]').is(':checked') ? true : false; - userAuth.checkToken((token) => { - $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user`, - data: { - username, - password, - admin: admin, - modify: true, - }, - type: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, - dataType: 'json', - success: (data) => { - if (data.error) { - showMessageBox(data.message); - } else { - showMessageBox({ - text: 'Update user basic information successfully', - dismissedCallback: () => { - $('#userEditModal').modal('hide'); - setAllUsers([]); - refreshAllUsers(); - }, - }); - } - }, - error: (xhr, textStatus, error) => { - $('#form-update-account').trigger('reset'); - const res = JSON.parse(xhr.responseText); - showMessageBox(res.message); - }, + updateUserAccountRequest(username, password, admin) + .then(updateUserInfoCallback) + .catch((err) => { + $('#form-update-account').trigger('reset'); + showMessageBox(err); }); - }); }; const updateUserGithubPAT = (username) => { const githubPAT = $('#form-update-github-token :input[name=githubPAT]').val(); - userAuth.checkToken((token) => { - $.ajax({ - url: `${webportalConfig.restServerUri}/api/v1/user/${username}/githubPAT`, - data: { - githubPAT: githubPAT, - }, - type: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, - dataType: 'json', - success: (data) => { - if (data.error) { - showMessageBox(data.message); - } else { - showMessageBox({ - text: 'Update user information successfully', - dismissedCallback: () => { - $('#userEditModal').modal('hide'); - setAllUsers([]); - refreshAllUsers(); - }, - }); - } - }, - error: (xhr, textStatus, error) => { - $('#form-update-github-token').trigger('reset'); - const res = JSON.parse(xhr.responseText); - showMessageBox(res.message); - }, + updateUserGithubPATRequest(username, githubPAT) + .then(updateUserInfoCallback) + .catch((err) => { + $('#form-update-github-token').trigger('reset'); + showMessageBox(err); }); - }); }; window.updateUserVc = updateUserVc; diff --git a/src/webportal/src/app/user/fabric/userView/utils.jsx b/src/webportal/src/app/user/fabric/userView/utils.js similarity index 100% rename from src/webportal/src/app/user/fabric/userView/utils.jsx rename to src/webportal/src/app/user/fabric/userView/utils.js From c876727078b133198b98399ca3f00651a1a72539 Mon Sep 17 00:00:00 2001 From: "Chao Li (MSRA)" Date: Wed, 8 May 2019 23:25:37 +0800 Subject: [PATCH 3/7] Resolve comments --- src/webportal/src/app/user/fabric/conn.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/webportal/src/app/user/fabric/conn.js b/src/webportal/src/app/user/fabric/conn.js index 371e60efcb..e2acadabab 100644 --- a/src/webportal/src/app/user/fabric/conn.js +++ b/src/webportal/src/app/user/fabric/conn.js @@ -46,13 +46,11 @@ export const removeUserRequest = async (username) => { headers: { 'Authorization': `Bearer ${token}`, }, - body: JSON.stringify({ - username: username, - }), + body: JSON.stringify({username}), }); }; -export const updateUserVcRequest = async (username, virtualCluster) => { +export const updateUserVcRequest = async (username, virtualClusters) => { const url = `${config.restServerUri}/api/v1/user/${username}/virtualClusters`; const token = checkToken(); return await fetchWrapper(url, { @@ -60,9 +58,7 @@ export const updateUserVcRequest = async (username, virtualCluster) => { headers: { 'Authorization': `Bearer ${token}`, }, - body: JSON.stringify({ - virtualClusters: virtualCluster, - }), + body: JSON.stringify({virtualClusters}), }); }; @@ -77,7 +73,7 @@ export const updateUserAccountRequest = async (username, password, admin) => { body: JSON.stringify({ username, password, - admin: admin, + admin, modify: true, }), }); @@ -91,8 +87,6 @@ export const updateUserGithubPATRequest = async (username, githubPAT) => { headers: { 'Authorization': `Bearer ${token}`, }, - body: JSON.stringify({ - githubPAT: githubPAT, - }), + body: JSON.stringify({githubPAT}), }); }; From 6d13dbb2462dca5d3c88aea4b2f522e08144b24f Mon Sep 17 00:00:00 2001 From: mslichao Date: Thu, 9 May 2019 11:33:40 +0800 Subject: [PATCH 4/7] Reslove comments --- .../src/app/user/fabric/userView/Ordering.jsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/webportal/src/app/user/fabric/userView/Ordering.jsx b/src/webportal/src/app/user/fabric/userView/Ordering.jsx index 1734db0f93..51dba1a6ce 100644 --- a/src/webportal/src/app/user/fabric/userView/Ordering.jsx +++ b/src/webportal/src/app/user/fabric/userView/Ordering.jsx @@ -29,22 +29,18 @@ export default class Ordering { apply(users) { const {field, descending} = this; - let comparator; if (field == null) { return users; } - if (field === 'username') { - comparator = descending - ? (a, b) => (String(b.username).localeCompare(a.username)) - : (a, b) => (String(a.username).localeCompare(b.username)); - } else if (field === 'admin') { - comparator = descending - ? (a, b) => toBool(b.admin) - toBool(a.admin) - : (a, b) => toBool(a.admin) - toBool(b.admin); - } else if (field === 'virtualCluster') { + let comparator; + if (field === 'virtualCluster') { comparator = descending ? (a, b) => String(getVirtualCluster(b)).localeCompare(getVirtualCluster(a)) : (a, b) => String(getVirtualCluster(a)).localeCompare(getVirtualCluster(b)); + } else { + comparator = descending + ? (a, b) => (String(b[field]).localeCompare(a[field])) + : (a, b) => (String(a[field]).localeCompare(b[field])); } return users.slice().sort(comparator); } From e44a2ab349a2833c893f5c44f3c9cdd84c4dbc58 Mon Sep 17 00:00:00 2001 From: mslichao Date: Thu, 9 May 2019 13:36:06 +0800 Subject: [PATCH 5/7] remove unused import --- src/webportal/src/app/user/fabric/userView/Ordering.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webportal/src/app/user/fabric/userView/Ordering.jsx b/src/webportal/src/app/user/fabric/userView/Ordering.jsx index 51dba1a6ce..6f020e24e6 100644 --- a/src/webportal/src/app/user/fabric/userView/Ordering.jsx +++ b/src/webportal/src/app/user/fabric/userView/Ordering.jsx @@ -15,7 +15,7 @@ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import {toBool, getVirtualCluster} from './utils'; +import {getVirtualCluster} from './utils'; export default class Ordering { /** From 0f9d57a9e6e4ab5f5b35a4b1c89b7fdd604851e3 Mon Sep 17 00:00:00 2001 From: mslichao Date: Thu, 9 May 2019 17:27:27 +0800 Subject: [PATCH 6/7] Resolve comments --- .../src/app/user/fabric/userView/Table.jsx | 13 +-- .../src/app/user/fabric/userView/TopBar.jsx | 34 ++++--- .../src/app/user/fabric/userView/index.jsx | 49 ++++++---- .../userView/user-edit-modal-component.ejs | 98 +++++++++---------- 4 files changed, 101 insertions(+), 93 deletions(-) diff --git a/src/webportal/src/app/user/fabric/userView/Table.jsx b/src/webportal/src/app/user/fabric/userView/Table.jsx index 6c78270c08..c57692224c 100644 --- a/src/webportal/src/app/user/fabric/userView/Table.jsx +++ b/src/webportal/src/app/user/fabric/userView/Table.jsx @@ -19,6 +19,9 @@ import React, {useContext, useMemo} from 'react'; import {ShimmeredDetailsList, Selection, FontClassNames, ColumnActionsMode, DefaultButton} from 'office-ui-fabric-react'; +import c from 'classnames'; +import t from '../../../components/tachyons.scss'; + import {toBool, getVirtualCluster} from './utils'; import Context from './Context'; @@ -123,16 +126,8 @@ export default function Table() { event.stopPropagation(); editUser(user); } - /** @type {React.CSSProperties} */ - const wrapperStyle = {display: 'inline-block', verticalAlign: 'middle', width: '100%'}; - const zeroPaddingRowFieldStyle = { - marginTop: -11, - marginBottom: -11, - marginLeft: -12, - marginRight: -8, - }; return ( -
+
diff --git a/src/webportal/src/app/user/fabric/userView/TopBar.jsx b/src/webportal/src/app/user/fabric/userView/TopBar.jsx index cc38d43d19..e837c80330 100644 --- a/src/webportal/src/app/user/fabric/userView/TopBar.jsx +++ b/src/webportal/src/app/user/fabric/userView/TopBar.jsx @@ -18,13 +18,15 @@ import React, {useContext, useMemo, useState} from 'react'; import {CommandBarButton, SearchBox, CommandBar, ContextualMenuItemType} from 'office-ui-fabric-react'; +import {PropTypes} from 'prop-types'; import {findIndex} from 'lodash'; +import t from '../../../components/tachyons.scss'; + import Context from './Context'; import Filter from './Filter'; import {toBool} from './utils'; -/* eslint-disable react/prop-types */ function FilterButton({defaultRender: Button, ...props}) { const {subMenuProps: {items}} = props; const checkedItems = items.filter((item) => item.checked).map((item) => item.text); @@ -38,6 +40,11 @@ function FilterButton({defaultRender: Button, ...props}) { ); } +FilterButton.propTypes = { + defaultRender: PropTypes.elementType.isRequired, + subMenuProps: PropTypes.object.isRequired, +}; + function KeywordSearchBox() { const {filter, setFilter} = useContext(Context); function onKeywordChange(keyword) { @@ -45,19 +52,16 @@ function KeywordSearchBox() { setFilter(new Filter(keyword, admins, virtualClusters)); } - /** @type {import('office-ui-fabric-react').IStyle} */ - const rootStyles = {backgroundColor: 'transparent', alignSelf: 'center', width: 220}; return ( ); } -/* eslint-enable react/prop-types */ function TopBar() { const [active, setActive] = useState(true); @@ -82,13 +86,15 @@ function TopBar() { return {admins, virtualClusters}; }, [allUsers]); + const transparentStyles = {root: [t.bgTransparent]}; + /** * @type {import('office-ui-fabric-react').ICommandBarItemProps} */ const btnAddUser = { key: 'addUser', name: 'Add User', - buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + buttonStyles: transparentStyles, iconProps: { iconName: 'Add', }, @@ -101,7 +107,7 @@ function TopBar() { const btnImportCSV = { key: 'importCSV', name: 'Import CSV', - buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + buttonStyles: transparentStyles, iconProps: { iconName: 'Stack', }, @@ -114,7 +120,7 @@ function TopBar() { const btnRemove = { key: 'remove', name: 'Remove', - buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + buttonStyles: transparentStyles, iconProps: { iconName: 'UserRemove', }, @@ -127,7 +133,7 @@ function TopBar() { const btnRefresh = { key: 'refresh', name: 'Refresh', - buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + buttonStyles: transparentStyles, iconProps: { iconName: 'Refresh', }, @@ -159,7 +165,7 @@ function TopBar() { onClick={item.onClick} iconProps={item.iconProps} menuIconProps={item.menuIconProps} - styles={{root: {backgroundColor: 'transparent'}}} + styles={transparentStyles} > Filters @@ -173,7 +179,7 @@ function TopBar() { const btnClear = { key: 'clear', name: 'Clear', - buttonStyles: {root: {backgroundColor: 'transparent', height: '100%'}}, + buttonStyles: {root: [t.bgTransparent, t.h100]}, iconOnly: true, iconProps: { iconName: 'Cancel', @@ -231,7 +237,7 @@ function TopBar() { return { key: 'admin', text: 'Admin', - buttonStyles: {root: {backgroundColor: 'transparent'}}, + buttonStyles: transparentStyles, iconProps: { iconName: 'Clock', }, @@ -298,7 +304,7 @@ function TopBar() { return { key: 'virtualCluster', name: 'Virtual Cluster', - buttonStyles: {root: {backgroundColor: 'transparent'}}, + buttonStyles: transparentStyles, iconProps: { iconName: 'CellPhone', }, @@ -338,7 +344,7 @@ function TopBar() { {active ? { - showEditInfo(user.username, user.admin, user.virtualCluster, user.hasGithubPAT); + setShowEditInfo({ + isOpen: true, + innerHtml: userEditModalComponent({ + 'username': user.username, + 'isAdmin': user.admin, + 'vcList': user.virtualCluster, + 'hasGithubPAT': String(user.hasGithubPAT), + }), + }); }; - const showEditInfo = (username, isAdmin, vcList, hasGithubPAT) => { - $('#modalPlaceHolder').html(userEditModalComponent({ - 'username': username, - 'isAdmin': String(isAdmin), - 'vcList': vcList, - 'hasGithubPAT': String(hasGithubPAT), - updateUserVc, - updateUserAccount, - updateUserGithubPAT, - })); - $('#userEditModal').modal('show'); + const hideEditUser = () => { + setShowEditInfo({isOpen: false, innerHtml: ''}); }; const updateUserInfoCallback = (data) => { @@ -180,7 +184,7 @@ export default function UserView() { showMessageBox({ text: 'Update user information successfully', dismissedCallback: () => { - $('#userEditModal').modal('hide'); + setShowEditInfo({isOpen: false, innerHtml: ''}); setAllUsers([]); refreshAllUsers(); }, @@ -222,6 +226,7 @@ export default function UserView() { window.updateUserVc = updateUserVc; window.updateUserAccount = updateUserAccount; window.updateUserGithubPAT = updateUserGithubPAT; + window.hideEditUser = hideEditUser; const context = { allUsers, @@ -242,14 +247,16 @@ export default function UserView() { editUser, }; + const {spacing} = getTheme(); + return ( - - + + - + @@ -257,9 +264,13 @@ export default function UserView() { + +
+ {loading.show && } {messageBox.text && } -
); } diff --git a/src/webportal/src/app/user/fabric/userView/user-edit-modal-component.ejs b/src/webportal/src/app/user/fabric/userView/user-edit-modal-component.ejs index 8568358af4..adc3546ba9 100644 --- a/src/webportal/src/app/user/fabric/userView/user-edit-modal-component.ejs +++ b/src/webportal/src/app/user/fabric/userView/user-edit-modal-component.ejs @@ -1,57 +1,53 @@ -