Merge branch 'origin/master' into 14348-cluster-search
authorDaniel Kos <daniel.kos@contractors.roche.com>
Fri, 28 Dec 2018 09:16:13 +0000 (10:16 +0100)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Fri, 28 Dec 2018 09:16:13 +0000 (10:16 +0100)
# Conflicts:
# src/views/compute-node-panel/compute-node-panel-root.tsx

Feature #14348

Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

32 files changed:
src/components/data-explorer/data-explorer.tsx
src/models/api-client-authorization.ts
src/models/group.ts
src/services/api/order-builder.test.ts
src/store/advanced-tab/advanced-tab.ts
src/store/api-client-authorizations/api-client-authorizations-actions.ts
src/store/api-client-authorizations/api-client-authorizations-middleware-service.ts [new file with mode: 0644]
src/store/api-client-authorizations/api-client-authorizations-reducer.ts [deleted file]
src/store/auth/auth-action-session.ts
src/store/context-menu/context-menu-actions.ts
src/store/projects/project-create-actions.ts
src/store/resources/resources-actions.ts
src/store/store.ts
src/store/users/users-actions.ts
src/store/workbench/workbench-actions.ts
src/views-components/context-menu/action-sets/user-action-set.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/dialog-create/dialog-project-create.tsx
src/views-components/dialog-create/dialog-user-create.tsx
src/views-components/dialog-forms/setup-shell-account-dialog.tsx [new file with mode: 0644]
src/views-components/form-fields/user-form-fields.tsx
src/views-components/project-properties/create-project-properties-form.tsx [new file with mode: 0644]
src/views-components/project-properties/create-project-properties-list.tsx [new file with mode: 0644]
src/views-components/resource-properties-form/resource-properties-form.tsx
src/views-components/search-bar/search-bar-view.tsx
src/views-components/user-dialog/manage-dialog.tsx [new file with mode: 0644]
src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx
src/views/api-client-authorization-panel/api-client-authorization-panel.tsx
src/views/compute-node-panel/compute-node-panel-root.tsx
src/views/my-account-panel/my-account-panel-root.tsx
src/views/user-panel/user-panel.tsx
src/views/workbench/workbench.tsx

index db9252362a634e6a0e6ffb71888ae7bc2cc78c49..d2b161edc24e9ba68e0a97c32de02f5d663efa36 100644 (file)
@@ -86,7 +86,7 @@ export const DataExplorer = withStyles(styles)(
                 paperKey, fetchMode
             } = this.props;
             return <Paper className={classes.root} {...paperProps} key={paperKey}>
-                <Toolbar className={classes.toolbar}>
+                {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={classes.toolbar}>
                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
                         {!hideSearchInput && <div className={classes.searchBox}>
                             <SearchInput
@@ -98,7 +98,7 @@ export const DataExplorer = withStyles(styles)(
                             columns={columns}
                             onColumnToggle={onColumnToggle} />}
                     </Grid>
-                </Toolbar>
+                </Toolbar>}
                 <DataTable
                     columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
                     items={items}
@@ -154,7 +154,6 @@ export const DataExplorer = withStyles(styles)(
             name: "Actions",
             selected: true,
             configurable: false,
-            sortDirection: SortDirection.NONE,
             filters: createTree(),
             key: "context-actions",
             render: this.renderContextMenuTrigger
index aff50be6d3aed62945ab5d02622c02ac243a39d4..01a92017d54d9ae9b8323d101f549359208323d5 100644 (file)
@@ -2,7 +2,9 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-export interface ApiClientAuthorization {
+import { Resource } from '~/models/resource';
+
+export interface ApiClientAuthorization extends Resource {
     uuid: string;
     apiToken: string;
     apiClientId: number;
index e2d0367a5e40fab7fef6fc7e98058c9e3840ee89..e13fbcbf05da2cb9584e21149020d3447350c0cd 100644 (file)
@@ -9,7 +9,7 @@ export interface GroupResource extends TrashableResource {
     name: string;
     groupClass: GroupClass | null;
     description: string;
-    properties: string;
+    properties: any;
     writeableBy: string[];
 }
 
index f56e0634357e2ad4ff3a8b27eba6fedec8ee493c..496b74a2563545c926c15bfdad24d4a3d9e5765f 100644 (file)
@@ -8,8 +8,8 @@ describe("OrderBuilder", () => {
     it("should build correct order query", () => {
         const order = new OrderBuilder()
             .addAsc("kind")
-            .addDesc("modifiedAt")
+            .addDesc("createdAt")
             .getOrder();
-        expect(order).toEqual("kind asc,modified_at desc");
+        expect(order).toEqual("kind asc,created_at desc");
     });
 });
index 0cb1c74038503c5c1f80fc58c36cb07d9f0803e5..921b1cd7d33784d88d19b9b1c092741595f2f904 100644 (file)
@@ -257,7 +257,8 @@ export const openAdvancedTabDialog = (uuid: string) =>
                 dispatch<any>(initAdvancedTabDialog(advanceDataComputeNode));
                 break;
             case ResourceKind.API_CLIENT_AUTHORIZATION:
-                const dataApiClientAuthorization = getState().apiClientAuthorizations.find(item => item.uuid === uuid);
+                const apiClientAuthorizationResources = getState().resources;
+                const dataApiClientAuthorization = getResource<ApiClientAuthorization>(uuid)(apiClientAuthorizationResources);
                 const advanceDataApiClientAuthorization = advancedTabData({
                     uuid,
                     metadata: '',
index 8ed8a3890e27e1405b90c1dc820685d610c7cc70..e4f9e9f7e52224bf5a2166caffb199ae920c1d8d 100644 (file)
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { RootState } from '~/store/store';
 import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
 import { ServiceRepository } from "~/services/services";
@@ -11,28 +10,26 @@ import { dialogActions } from '~/store/dialog/dialog-actions';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { navigateToRootProject } from '~/store/navigation/navigation-action';
 import { ApiClientAuthorization } from '~/models/api-client-authorization';
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { getResource } from '~/store/resources/resources';
 
-export const apiClientAuthorizationsActions = unionize({
-    SET_API_CLIENT_AUTHORIZATIONS: ofType<ApiClientAuthorization[]>(),
-    REMOVE_API_CLIENT_AUTHORIZATION: ofType<string>()
-});
 
-export type ApiClientAuthorizationsActions = UnionOf<typeof apiClientAuthorizationsActions>;
+export const API_CLIENT_AUTHORIZATION_PANEL_ID = 'apiClientAuthorizationPanelId';
+export const apiClientAuthorizationsActions = bindDataExplorerActions(API_CLIENT_AUTHORIZATION_PANEL_ID);
 
 export const API_CLIENT_AUTHORIZATION_REMOVE_DIALOG = 'apiClientAuthorizationRemoveDialog';
 export const API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG = 'apiClientAuthorizationAttributesDialog';
 export const API_CLIENT_AUTHORIZATION_HELP_DIALOG = 'apiClientAuthorizationHelpDialog';
 
+
 export const loadApiClientAuthorizationsPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const user = getState().auth.user;
         if (user && user.isAdmin) {
             try {
                 dispatch(setBreadcrumbs([{ label: 'Api client authorizations' }]));
-                const response = await services.apiClientAuthorizationService.list();
-                dispatch(apiClientAuthorizationsActions.SET_API_CLIENT_AUTHORIZATIONS(response.items));
+                dispatch(apiClientAuthorizationsActions.REQUEST_ITEMS());
             } catch (e) {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000 }));
                 return;
             }
         } else {
@@ -43,7 +40,8 @@ export const loadApiClientAuthorizationsPanel = () =>
 
 export const openApiClientAuthorizationAttributesDialog = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const apiClientAuthorization = getState().apiClientAuthorizations.find(node => node.uuid === uuid);
+        const { resources } = getState();
+        const apiClientAuthorization = getResource<ApiClientAuthorization>(uuid)(resources);
         dispatch(dialogActions.OPEN_DIALOG({ id: API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG, data: { apiClientAuthorization } }));
     };
 
@@ -65,7 +63,7 @@ export const removeApiClientAuthorization = (uuid: string) =>
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
         try {
             await services.apiClientAuthorizationService.delete(uuid);
-            dispatch(apiClientAuthorizationsActions.REMOVE_API_CLIENT_AUTHORIZATION(uuid));
+            dispatch(apiClientAuthorizationsActions.REQUEST_ITEMS());
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Api client authorization has been successfully removed.', hideDuration: 2000 }));
         } catch (e) {
             return;
diff --git a/src/store/api-client-authorizations/api-client-authorizations-middleware-service.ts b/src/store/api-client-authorizations/api-client-authorizations-middleware-service.ts
new file mode 100644 (file)
index 0000000..99e2a95
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateResources } from '~/store/resources/resources-actions';
+import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { apiClientAuthorizationsActions } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { ListResults } from '~/services/common-service/common-service';
+import { ApiClientAuthorization } from '~/models/api-client-authorization';
+import { ApiClientAuthorizationPanelColumnNames } from '~/views/api-client-authorization-panel/api-client-authorization-panel-root';
+import { SortDirection } from '~/components/data-table/data-column';
+
+export class ApiClientAuthorizationMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(private services: ServiceRepository, id: string) {
+        super(id);
+    }
+
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        const state = api.getState();
+        const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+        try {
+            const response = await this.services.apiClientAuthorizationService.list(getParams(dataExplorer));
+            api.dispatch(updateResources(response.items));
+            api.dispatch(setItems(response));
+        } catch {
+            api.dispatch(couldNotFetchLinks());
+        }
+    }
+}
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    order: getOrder(dataExplorer)
+});
+
+const getOrder = (dataExplorer: DataExplorer) => {
+    const sortColumn = getSortColumn(dataExplorer);
+    const order = new OrderBuilder<ApiClientAuthorization>();
+    if (sortColumn) {
+        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+            ? OrderDirection.ASC
+            : OrderDirection.DESC;
+
+        const columnName = sortColumn && sortColumn.name === ApiClientAuthorizationPanelColumnNames.UUID ? "uuid" : "updatedAt";
+        return order
+            .addOrder(sortDirection, columnName)
+            .getOrder();
+    } else {
+        return order.getOrder();
+    }
+};
+
+export const setItems = (listResults: ListResults<ApiClientAuthorization>) =>
+    apiClientAuthorizationsActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+const couldNotFetchLinks = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch api client authorizations.',
+        kind: SnackbarKind.ERROR
+    });
diff --git a/src/store/api-client-authorizations/api-client-authorizations-reducer.ts b/src/store/api-client-authorizations/api-client-authorizations-reducer.ts
deleted file mode 100644 (file)
index 7084dea..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { 
-    apiClientAuthorizationsActions, 
-    ApiClientAuthorizationsActions 
-} from '~/store/api-client-authorizations/api-client-authorizations-actions';
-import { ApiClientAuthorization } from '~/models/api-client-authorization';
-
-export type ApiClientAuthorizationsState = ApiClientAuthorization[];
-
-const initialState: ApiClientAuthorizationsState = [];
-
-export const apiClientAuthorizationsReducer = 
-    (state: ApiClientAuthorizationsState = initialState, action: ApiClientAuthorizationsActions): ApiClientAuthorizationsState =>
-        apiClientAuthorizationsActions.match(action, {
-            SET_API_CLIENT_AUTHORIZATIONS: apiClientAuthorizations => apiClientAuthorizations,
-            REMOVE_API_CLIENT_AUTHORIZATION: (uuid: string) => 
-                state.filter((apiClientAuthorization) => apiClientAuthorization.uuid !== uuid),
-            default: () => state
-        });
\ No newline at end of file
index 43cf9f6d21ee55879792d3185388a080cbecefcb..b32e205016a1824a8c6ed3554637ffac68b2b726 100644 (file)
@@ -80,7 +80,7 @@ const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) =>
     return `v2/${tokenUuid}/${hmac}`;
 };
 
-const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{user: User, token: string}> => {
+const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{ user: User, token: string }> => {
     const tokenUuid = await getTokenUuid(activeSession.baseUrl, activeSession.token);
     const saltedToken = getSaltedToken(clusterId, tokenUuid, activeSession.token);
     const user = await getUserDetails(baseUrl, saltedToken);
@@ -207,7 +207,7 @@ export const initSessions = (authService: AuthService, config: Config, user: Use
 export const loadSiteManagerPanel = () =>
     async (dispatch: Dispatch<any>) => {
         try {
-            dispatch(setBreadcrumbs([{ label: 'Site Manager'}]));
+            dispatch(setBreadcrumbs([{ label: 'Site Manager' }]));
             dispatch(validateSessions());
         } catch (e) {
             return;
index c43d5685655c8667bc272246bedf6597b41ca7c9..ca89f3eb3fe87cf9b220299ca764ea652c535cba 100644 (file)
@@ -123,12 +123,12 @@ export const openComputeNodeContextMenu = (event: React.MouseEvent<HTMLElement>,
     };
 
 export const openApiClientAuthorizationContextMenu =
-    (event: React.MouseEvent<HTMLElement>, apiClientAuthorization: ApiClientAuthorization) =>
+    (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
         (dispatch: Dispatch) => {
             dispatch<any>(openContextMenu(event, {
                 name: '',
-                uuid: apiClientAuthorization.uuid,
-                ownerUuid: apiClientAuthorization.ownerUuid,
+                uuid: resourceUuid,
+                ownerUuid: '',
                 kind: ResourceKind.API_CLIENT_AUTHORIZATION,
                 menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION
             }));
index d226048b08c06b4e1fd9ca868bf63ea2ddd92beb..ddcd233fd5ad8053dbe60819bdfdf63f364335bb 100644 (file)
@@ -3,21 +3,29 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { reset, startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
+import { reset, startSubmit, stopSubmit, initialize, FormErrors, formValueSelector, change } from 'redux-form';
 import { RootState } from '~/store/store';
 import { dialogActions } from "~/store/dialog/dialog-actions";
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
 import { ProjectResource } from '~/models/project';
 import { ServiceRepository } from '~/services/services';
 import { matchProjectRoute, matchRunProcessRoute } from '~/routes/routes';
+import { ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form';
 
 export interface ProjectCreateFormDialogData {
     ownerUuid: string;
     name: string;
     description: string;
+    properties: ProjectProperties;
+}
+
+export interface ProjectProperties {
+    [key: string]: string;
 }
 
 export const PROJECT_CREATE_FORM_NAME = 'projectCreateFormName';
+export const PROJECT_CREATE_PROPERTIES_FORM_NAME = 'projectCreatePropertiesFormName';
+export const PROJECT_CREATE_FORM_SELECTOR = formValueSelector(PROJECT_CREATE_FORM_NAME);
 
 export const isProjectOrRunProcessRoute = ({ router }: RootState) => {
     const pathname = router.location ? router.location.pathname : '';
@@ -63,3 +71,17 @@ export const createProject = (project: Partial<ProjectResource>) =>
             return undefined;
         }
     };
+
+export const addPropertyToCreateProjectForm = (data: ResourcePropertiesFormData) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
+        properties[data.key] = data.value;
+        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties ));
+    };
+
+export const removePropertyFromCreateProjectForm = (key: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
+        delete properties[key];
+        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties ));
+    };
\ No newline at end of file
index 0453236a3f797e97e39c2c2027312b5124e68796..1de2feff8b9f8d42111090f132f7dfba4a624a0c 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from '~/common/unionize';
-import { Resource, extractUuidKind } from '~/models/resource';
+import { extractUuidKind, Resource } from '~/models/resource';
 import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
index 4ad7ca45f3f5c25b4446ab22eafa4909a4835597..f842b0c6efadc59f43b812bea337ee32663ee996 100644 (file)
@@ -48,7 +48,6 @@ import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
 import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer';
 import { UserMiddlewareService } from '~/store/users/user-panel-middleware-service';
 import { USERS_PANEL_ID } from '~/store/users/users-actions';
-import { apiClientAuthorizationsReducer } from '~/store/api-client-authorizations/api-client-authorizations-reducer';
 import { GroupsPanelMiddlewareService } from '~/store/groups-panel/groups-panel-middleware-service';
 import { GROUPS_PANEL_ID } from '~/store/groups-panel/groups-panel-actions';
 import { GroupDetailsPanelMiddlewareService } from '~/store/group-details-panel/group-details-panel-middleware-service';
@@ -57,6 +56,8 @@ import { LINK_PANEL_ID } from '~/store/link-panel/link-panel-actions';
 import { LinkMiddlewareService } from '~/store/link-panel/link-panel-middleware-service';
 import { COMPUTE_NODE_PANEL_ID } from '~/store/compute-nodes/compute-nodes-actions';
 import { ComputeNodeMiddlewareService } from '~/store/compute-nodes/compute-nodes-middleware-service';
+import { API_CLIENT_AUTHORIZATION_PANEL_ID } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { ApiClientAuthorizationMiddlewareService } from '~/store/api-client-authorizations/api-client-authorizations-middleware-service';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -97,13 +98,15 @@ export function configureStore(history: History, services: ServiceRepository): R
     const groupDetailsPanelMiddleware = dataExplorerMiddleware(
         new GroupDetailsPanelMiddlewareService(services, GROUP_DETAILS_PANEL_ID)
     );
-
     const linkPanelMiddleware = dataExplorerMiddleware(
         new LinkMiddlewareService(services, LINK_PANEL_ID)
     );
     const computeNodeMiddleware = dataExplorerMiddleware(
         new ComputeNodeMiddlewareService(services, COMPUTE_NODE_PANEL_ID)
     );
+    const apiClientAuthorizationMiddlewareService = dataExplorerMiddleware(
+        new ApiClientAuthorizationMiddlewareService(services, API_CLIENT_AUTHORIZATION_PANEL_ID)
+    );
     const middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware.withExtraArgument(services),
@@ -118,6 +121,7 @@ export function configureStore(history: History, services: ServiceRepository): R
         groupDetailsPanelMiddleware,
         linkPanelMiddleware,
         computeNodeMiddleware,
+        apiClientAuthorizationMiddlewareService
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
@@ -148,6 +152,5 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     searchBar: searchBarReducer,
     virtualMachines: virtualMachinesReducer,
     repositories: repositoriesReducer,
-    keepServices: keepServicesReducer,
-    apiClientAuthorizations: apiClientAuthorizationsReducer
+    keepServices: keepServicesReducer
 });
index 585a3663bcdf5fabf7b34fe295f0d30981f08fd0..42406858623aa9f0f99dde829ae4fca1d99b6948 100644 (file)
@@ -12,14 +12,16 @@ import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions
 import { UserResource } from "~/models/user";
 import { getResource } from '~/store/resources/resources';
 import { navigateToProject, navigateToUsers, navigateToRootProject } from "~/store/navigation/navigation-action";
+import { saveApiToken, getUserDetails } from '~/store/auth/auth-action';
 
 export const USERS_PANEL_ID = 'usersPanel';
 export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
 export const USER_CREATE_FORM_NAME = 'userCreateFormName';
+export const USER_MANAGEMENT_DIALOG = 'userManageDialog';
+export const SETUP_SHELL_ACCOUNT_DIALOG = 'setupShellAccountDialog';
 
 export interface UserCreateFormDialogData {
     email: string;
-    identityUrl: string;
     virtualMachineName: string;
     groupVirtualMachine: string;
 }
@@ -31,6 +33,35 @@ export const openUserAttributes = (uuid: string) =>
         dispatch(dialogActions.OPEN_DIALOG({ id: USER_ATTRIBUTES_DIALOG, data }));
     };
 
+export const openUserManagement = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { resources } = getState();
+        const user = getResource<UserResource>(uuid)(resources);
+        const clients = await services.apiClientAuthorizationService.list();
+        const client = clients.items.find(it => it.ownerUuid === uuid);
+        console.log(client);
+        dispatch(dialogActions.OPEN_DIALOG({ id: USER_MANAGEMENT_DIALOG, data: { user, client } }));
+    };
+
+export const openSetupShellAccount = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { resources } = getState();
+        const user = getResource<UserResource>(uuid)(resources);
+        const virtualMachines = await services.virtualMachineService.list();
+        dispatch(dialogActions.CLOSE_DIALOG({ id: USER_MANAGEMENT_DIALOG }));
+        dispatch(dialogActions.OPEN_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG, data: { user, ...virtualMachines } }));
+    };
+
+export const loginAs = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const client = await services.apiClientAuthorizationService.get(uuid);
+        dispatch<any>(saveApiToken(client.apiToken));
+        dispatch<any>(getUserDetails()).then(() => {
+            location.reload();
+            dispatch<any>(navigateToRootProject);
+        });
+    };
+
 export const openUserCreateDialog = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const userUuid = await services.authService.getUuid();
index d01143db7eb09d6036dd492ba0eb1284cfc74567..8f5bb605179045b21570b8ad7495a42d4cd27954 100644 (file)
@@ -85,7 +85,8 @@ import { computeNodesActions, loadComputeNodesPanel } from '~/store/compute-node
 import { linkPanelColumns } from '~/views/link-panel/link-panel-root';
 import { userPanelColumns } from '~/views/user-panel/user-panel';
 import { computeNodePanelColumns } from '~/views/compute-node-panel/compute-node-panel-root';
-import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { apiClientAuthorizationPanelColumns } from '~/views/api-client-authorization-panel/api-client-authorization-panel-root';
 import * as groupPanelActions from '~/store/groups-panel/groups-panel-actions';
 import { groupsPanelColumns } from '~/views/groups-panel/groups-panel';
 import * as groupDetailsPanelActions from '~/store/group-details-panel/group-details-panel-actions';
@@ -130,6 +131,8 @@ export const loadWorkbench = () =>
                 dispatch(groupDetailsPanelActions.GroupDetailsPanelActions.SET_COLUMNS({columns: groupDetailsPanelColumns}));
                 dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
                 dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
+                dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
+
                 dispatch<any>(initSidePanelTree());
                 if (router.location) {
                     const match = matchRootRoute(router.location.pathname);
index 7b0884e668b02ef9f3c5f30bf4c2b2eab3607873..d2b97d15525755f6d66260ff6b78bdf7f9d3400e 100644 (file)
@@ -5,7 +5,7 @@
 import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
 import { AdvancedIcon, ProjectIcon, AttributesIcon, UserPanelIcon } from "~/components/icon/icon";
 import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
-import { openUserAttributes, openUserProjects } from "~/store/users/users-actions";
+import { openUserAttributes, openUserProjects, openUserManagement } from "~/store/users/users-actions";
 
 export const userActionSet: ContextMenuActionSet = [[{
     name: "Attributes",
@@ -25,11 +25,10 @@ export const userActionSet: ContextMenuActionSet = [[{
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
     }
-},
-{
+}, {
     name: "Manage",
     icon: UserPanelIcon,
     execute: (dispatch, { uuid }) => {
-        dispatch<any>(openAdvancedTabDialog(uuid));
+        dispatch<any>(openUserManagement(uuid));
     }
 }]];
index f90dac6b22c46c276afcb03db0b38fb6564d9b5e..d52dcab1b9e31fa7a4978b2e65d4794904e829bb 100644 (file)
@@ -191,14 +191,35 @@ export const ResourceUsername = connect(
         return resource || { username: '' };
     })(renderUsername);
 
-// Compute Node Resources
-const renderNodeDate = (date: string) =>
+// Common methods
+const renderCommonData = (data: string) =>
+    <Typography noWrap>{data}</Typography>;
+
+const renderCommonDate = (date: string) =>
     <Typography noWrap>{formatDate(date)}</Typography>;
 
-const renderNodeData = (data: string) => {
-    return <Typography noWrap>{data}</Typography>;
-};
+export const CommonUuid = withResourceData('uuid', renderCommonData);
+
+// Api Client Authorizations
+export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
+
+export const TokenApiToken = withResourceData('apiToken', renderCommonData);
+
+export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
+
+export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
 
+export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
+
+export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
+
+export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
+
+export const TokenScopes = withResourceData('scopes', renderCommonData);
+
+export const TokenUserId = withResourceData('userId', renderCommonData);
+
+// Compute Node Resources
 const renderNodeInfo = (data: string) => {
     return <Typography>{JSON.stringify(data, null, 4)}</Typography>;
 };
@@ -227,19 +248,17 @@ export const ResourceCluster = (props: { uuid: string }) => {
 
 export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
 
-export const ComputeNodeUuid = withResourceData('uuid', renderNodeData);
-
-export const ComputeNodeDomain = withResourceData('domain', renderNodeData);
+export const ComputeNodeDomain = withResourceData('domain', renderCommonData);
 
-export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderNodeDate);
+export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderCommonDate);
 
-export const ComputeNodeHostname = withResourceData('hostname', renderNodeData);
+export const ComputeNodeHostname = withResourceData('hostname', renderCommonData);
 
-export const ComputeNodeIpAddress = withResourceData('ipAddress', renderNodeData);
+export const ComputeNodeIpAddress = withResourceData('ipAddress', renderCommonData);
 
-export const ComputeNodeJobUuid = withResourceData('jobUuid', renderNodeData);
+export const ComputeNodeJobUuid = withResourceData('jobUuid', renderCommonData);
 
-export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderNodeDate);
+export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderCommonDate);
 
 // Links Resources
 const renderLinkName = (item: { name: string }) =>
index 3075d96e4b722f42c4c248ee65a7d844c13bdeef..02fb67e5f202c8b70b96c1bb59f76587998edf40 100644 (file)
@@ -8,10 +8,12 @@ import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
+import { CreateProjectPropertiesForm } from '~/views-components/project-properties/create-project-properties-form';
+import { CreateProjectPropertiesList } from '~/views-components/project-properties/create-project-properties-list';
 
-type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
+type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
 
-export const DialogProjectCreate = (props: DialogCollectionProps) =>
+export const DialogProjectCreate = (props: DialogProjectProps) =>
     <FormDialog
         dialogTitle='New project'
         formFields={ProjectAddFields}
@@ -22,4 +24,6 @@ export const DialogProjectCreate = (props: DialogCollectionProps) =>
 const ProjectAddFields = () => <span>
     <ProjectNameField />
     <ProjectDescriptionField />
+    <CreateProjectPropertiesForm />
+    <CreateProjectPropertiesList />
 </span>;
index 14365af7d6c69f1d8dddc91fc486b6d807969326..06db587307a3119e06ae38d8cdb7c6211b18dd69 100644 (file)
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { InjectedFormProps } from 'redux-form';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { UserEmailField, UserIdentityUrlField, UserVirtualMachineField, UserGroupsVirtualMachineField } from '~/views-components/form-fields/user-form-fields';
+import { UserEmailField, UserVirtualMachineField, UserGroupsVirtualMachineField } from '~/views-components/form-fields/user-form-fields';
 
 export type DialogUserProps = WithDialogProps<{}> & InjectedFormProps<any>;
 
@@ -20,7 +20,6 @@ export const UserRepositoryCreate = (props: DialogUserProps) =>
 
 const UserAddFields = (props: DialogUserProps) => <span>
     <UserEmailField />
-    <UserIdentityUrlField />
     <UserVirtualMachineField data={props.data}/>
     <UserGroupsVirtualMachineField />
 </span>;
diff --git a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx b/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
new file mode 100644 (file)
index 0000000..c53f53c
--- /dev/null
@@ -0,0 +1,95 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import * as React from 'react';
+import { compose } from "redux";
+import { reduxForm, InjectedFormProps, Field } from 'redux-form';
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { TextField } from '~/components/text-field/text-field';
+import { VirtualMachinesResource } from '~/models/virtual-machines';
+import { USER_LENGTH_VALIDATION } from '~/validators/validators';
+import { InputLabel } from '@material-ui/core';
+import { NativeSelectField } from '~/components/select-field/select-field';
+import { SETUP_SHELL_ACCOUNT_DIALOG, createUser } from '~/store/users/users-actions';
+import { UserResource } from '~/models/user';
+
+interface SetupShellAccountFormDialogData {
+    email: string;
+    virtualMachineName: string;
+    groupVirtualMachine: string;
+}
+
+export const SetupShellAccountDialog = compose(
+    withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
+    reduxForm<SetupShellAccountFormDialogData>({
+        form: SETUP_SHELL_ACCOUNT_DIALOG,
+        onSubmit: (data, dispatch) => {
+            dispatch(createUser(data));
+        }
+    })
+)(
+    (props: SetupShellAccountDialogComponentProps) =>
+        <FormDialog
+            dialogTitle='Setup shell account'
+            formFields={SetupShellAccountFormFields}
+            submitLabel='Submit'
+            {...props}
+        />
+);
+
+interface UserProps {
+    data: {
+        user: UserResource;
+    };
+}
+
+interface VirtualMachinesProps {
+    data: {
+        items: VirtualMachinesResource[];
+    };
+}
+interface DataProps {
+    user: UserResource;
+    items: VirtualMachinesResource[];
+}
+
+const UserEmailField = ({ data }: UserProps) =>
+    <span>
+        <Field
+            name='email'
+            component={TextField}
+            disabled
+            label={data.user.email} /></span>;
+
+const UserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
+    <div style={{ marginBottom: '21px' }}>
+        <InputLabel>Virtual Machine</InputLabel>
+        <Field
+            name='virtualMachine'
+            component={NativeSelectField}
+            validate={USER_LENGTH_VALIDATION}
+            items={getVirtualMachinesList(data.items)} />
+    </div>;
+
+const UserGroupsVirtualMachineField = () =>
+    <Field
+        name='groups'
+        component={TextField}
+        validate={USER_LENGTH_VALIDATION}
+        label="Groups for virtual machine (comma separated list)" />;
+
+const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
+    virtualMachines.map(it => ({ key: it.hostname, value: it.hostname }));
+
+type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<SetupShellAccountFormDialogData>;
+
+const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProps) =>
+    <>
+        <UserEmailField data={props.data as DataProps} />
+        <UserVirtualMachineField data={props.data as DataProps} />
+        <UserGroupsVirtualMachineField />
+    </>;
+
+
+
index 856344492f72f6fe361a2e0c6858f9f0c347a29b..11d7d80280c699d5bd55c88781db65459d905ab6 100644 (file)
@@ -18,13 +18,6 @@ export const UserEmailField = () =>
         autoFocus={true}
         label="Email" />;
 
-export const UserIdentityUrlField = () =>
-    <Field
-        name='identityUrl'
-        component={TextField}
-        validate={USER_LENGTH_VALIDATION}
-        label="Identity URL Prefix" />;
-
 export const UserVirtualMachineField = ({ data }: any) =>
     <div style={{ marginBottom: '21px' }}>
         <InputLabel>Virtual Machine</InputLabel>
diff --git a/src/views-components/project-properties/create-project-properties-form.tsx b/src/views-components/project-properties/create-project-properties-form.tsx
new file mode 100644 (file)
index 0000000..385afff
--- /dev/null
@@ -0,0 +1,54 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, reset, InjectedFormProps } from 'redux-form';
+import { PROJECT_CREATE_PROPERTIES_FORM_NAME, addPropertyToCreateProjectForm } from '~/store/projects/project-create-actions';
+import { ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form';
+import { StyleRulesCallback, WithStyles, withStyles, Grid } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { PropertyKeyField } from '~/views-components/resource-properties-form/property-key-field';
+import { PropertyValueField } from '~/views-components/resource-properties-form/property-value-field';
+import { Button } from '~/views-components/resource-properties-form/resource-properties-form';
+
+type CssRules = 'root';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        paddingTop: theme.spacing.unit,
+        margin: 0
+    }
+});
+
+type CreateProjectPropertiesFormProps = InjectedFormProps<ResourcePropertiesFormData> & WithStyles<CssRules>;
+
+const Form = withStyles(styles)(
+    ({ handleSubmit, submitting, invalid, classes }: CreateProjectPropertiesFormProps) =>
+        <Grid container spacing={16} className={classes.root}>
+            <Grid item xs={5}>
+                <PropertyKeyField />
+            </Grid>
+            <Grid item xs={5}>
+                <PropertyValueField />
+            </Grid>
+            <Grid item xs={2}>
+                <Button
+                    disabled={invalid}
+                    loading={submitting}
+                    color='primary'
+                    variant='contained'
+                    onClick={handleSubmit}>
+                    Add
+                </Button>
+            </Grid>
+        </Grid>
+);
+
+export const CreateProjectPropertiesForm = reduxForm<ResourcePropertiesFormData>({
+    form: PROJECT_CREATE_PROPERTIES_FORM_NAME,
+    onSubmit: (data, dispatch) => {
+        dispatch(addPropertyToCreateProjectForm(data));
+        dispatch(reset(PROJECT_CREATE_PROPERTIES_FORM_NAME));
+    }
+})(Form);
\ No newline at end of file
diff --git a/src/views-components/project-properties/create-project-properties-list.tsx b/src/views-components/project-properties/create-project-properties-list.tsx
new file mode 100644 (file)
index 0000000..1d2050f
--- /dev/null
@@ -0,0 +1,54 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { Dispatch } from 'redux';
+import { withStyles, StyleRulesCallback, WithStyles, Chip } from '@material-ui/core';
+import { RootState } from '~/store/store';
+import { removePropertyFromCreateProjectForm, PROJECT_CREATE_FORM_SELECTOR, ProjectProperties } from '~/store/projects/project-create-actions';
+import { ArvadosTheme } from '~/common/custom-theme';
+
+type CssRules = 'tag';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    tag: {
+        marginRight: theme.spacing.unit,
+        marginBottom: theme.spacing.unit
+    }
+});
+
+interface CreateProjectPropertiesListDataProps {
+    properties: ProjectProperties;
+}
+
+interface CreateProjectPropertiesListActionProps {
+    handleDelete: (key: string) => void;
+}
+
+const mapStateToProps = (state: RootState): CreateProjectPropertiesListDataProps => {
+    const properties = PROJECT_CREATE_FORM_SELECTOR(state, 'properties');
+    return { properties };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): CreateProjectPropertiesListActionProps => ({
+    handleDelete: (key: string) => dispatch<any>(removePropertyFromCreateProjectForm(key))
+});
+
+type CreateProjectPropertiesListProps = CreateProjectPropertiesListDataProps & 
+    CreateProjectPropertiesListActionProps & WithStyles<CssRules>;
+
+const List = withStyles(styles)(
+    ({ classes, handleDelete, properties }: CreateProjectPropertiesListProps) =>
+        <div>
+            {properties &&
+                Object.keys(properties).map(k => {
+                    return <Chip key={k} className={classes.tag}
+                        onDelete={() => handleDelete(k)}
+                        label={`${k}: ${properties[k]}`} />;
+                })}
+        </div>
+);
+
+export const CreateProjectPropertiesList = connect(mapStateToProps, mapDispatchToProps)(List);
\ No newline at end of file
index a62b3d1563a8459333d7ae1be58c62ca97812256..6c2e025a0834f85d4c816f492ec717f6f0d164fb 100644 (file)
@@ -39,6 +39,6 @@ export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, clas
         </Grid>
     </form>;
 
-const Button = withStyles(theme => ({
+export const Button = withStyles(theme => ({
     root: { marginTop: theme.spacing.unit }
 }))(ProgressButton);
index 8d767b2b6affc816fcd57f2fcc94df7f2ca4c266..6251308d984f74dd31e3531d330c2fe94673a8ee 100644 (file)
@@ -11,7 +11,6 @@ import {
     WithStyles,
     Tooltip,
     InputAdornment, Input,
-    Popover,
 } from '@material-ui/core';
 import SearchIcon from '@material-ui/icons/Search';
 import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
@@ -41,12 +40,14 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
         container: {
             position: 'relative',
             width: '100%',
-            borderRadius: theme.spacing.unit / 2
+            borderRadius: theme.spacing.unit / 2,
+            zIndex: theme.zIndex.modal,
         },
         containerSearchViewOpened: {
             position: 'relative',
             width: '100%',
-            borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`
+            borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`,
+            zIndex: theme.zIndex.modal,
         },
         input: {
             border: 'none',
@@ -144,99 +145,53 @@ const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) =>
 };
 
 export const SearchBarView = withStyles(styles)(
-    class SearchBarView extends React.Component<SearchBarViewProps> {
-
-        viewAnchorRef = React.createRef<HTMLDivElement>();
-
-        render() {
-            const { children, ...props } = this.props;
-            const { classes } = props;
-            return (
-                <Paper className={classes.container}>
-                    <div ref={this.viewAnchorRef}>
-                        <form onSubmit={props.onSubmit}>
-                            <SearchInput {...props} />
-                        </form>
+    (props: SearchBarViewProps) => {
+        const { classes, isPopoverOpen } = props;
+        return (
+            <>
+
+                {isPopoverOpen &&
+                    <Backdrop onClick={props.closeView} />}
+
+                <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
+                    <form onSubmit={props.onSubmit}>
+                        <Input
+                            className={classes.input}
+                            onChange={props.onChange}
+                            placeholder="Search"
+                            value={props.searchValue}
+                            fullWidth={true}
+                            disableUnderline={true}
+                            onClick={e => handleInputClick(e, props)}
+                            onKeyDown={e => handleKeyDown(e, props)}
+                            startAdornment={
+                                <InputAdornment position="start">
+                                    <Tooltip title='Search'>
+                                        <IconButton type="submit">
+                                            <SearchIcon />
+                                        </IconButton>
+                                    </Tooltip>
+                                </InputAdornment>
+                            }
+                            endAdornment={
+                                <InputAdornment position="end">
+                                    <Tooltip title='Advanced search'>
+                                        <IconButton onClick={e => handleDropdownClick(e, props)}>
+                                            <ArrowDropDownIcon />
+                                        </IconButton>
+                                    </Tooltip>
+                                </InputAdornment>
+                            } />
+                    </form>
+                    <div className={classes.view}>
+                        {isPopoverOpen && getView({ ...props })}
                     </div>
-                    <SearchViewContainer
-                        {...props}
-                        width={this.getViewWidth()}
-                        anchorEl={this.viewAnchorRef.current}>
-                        <form onSubmit={props.onSubmit}>
-                            <SearchInput
-                                {...props}
-                                autoFocus
-                                disableClickHandler />
-                        </form>
-                        {getView({ ...props })}
-                    </SearchViewContainer>
                 </Paper >
-            );
-        }
-
-        getViewWidth() {
-            const { current } = this.viewAnchorRef;
-            return current ? current.offsetWidth : 0;
-        }
+            </>
+        );
     }
-
 );
 
-const SearchInput = (props: SearchBarViewProps & { disableClickHandler?: boolean; autoFocus?: boolean }) => {
-    const { classes } = props;
-    return <Input
-        autoFocus={props.autoFocus}
-        className={classes.input}
-        onChange={props.onChange}
-        placeholder="Search"
-        value={props.searchValue}
-        fullWidth={true}
-        disableUnderline={true}
-        onClick={e => !props.disableClickHandler && handleInputClick(e, props)}
-        onKeyDown={e => handleKeyDown(e, props)}
-        startAdornment={
-            <InputAdornment position="start">
-                <Tooltip title='Search'>
-                    <IconButton type="submit">
-                        <SearchIcon />
-                    </IconButton>
-                </Tooltip>
-            </InputAdornment>
-        }
-        endAdornment={
-            <InputAdornment position="end">
-                <Tooltip title='Advanced search'>
-                    <IconButton onClick={e => handleDropdownClick(e, props)}>
-                        <ArrowDropDownIcon />
-                    </IconButton>
-                </Tooltip>
-            </InputAdornment>
-        } />;
-};
-
-const SearchViewContainer = (props: SearchBarViewProps & { width: number, anchorEl: HTMLElement | null, children: React.ReactNode }) =>
-    <Popover
-        PaperProps={{
-            style: { width: props.width }
-        }}
-        anchorEl={props.anchorEl}
-        anchorOrigin={{
-            vertical: 'top',
-            horizontal: 'center',
-        }}
-        transformOrigin={{
-            vertical: 'top',
-            horizontal: 'center',
-        }}
-        disableAutoFocus
-        open={props.isPopoverOpen}
-        onClose={props.closeView}>
-        {
-            props.children
-        }
-    </Popover>;
-
-
 const getView = (props: SearchBarViewProps) => {
     switch (props.currentView) {
         case SearchView.AUTOCOMPLETE:
@@ -261,3 +216,16 @@ const getView = (props: SearchBarViewProps) => {
                 selectedItem={props.selectedItem} />;
     }
 };
+
+const Backdrop = withStyles<'backdrop'>(theme => ({
+    backdrop: {
+        position: 'fixed',
+        top: 0,
+        right: 0,
+        bottom: 0,
+        left: 0,
+        zIndex: theme.zIndex.modal
+    }
+}))(
+    ({ classes, ...props }: WithStyles<'backdrop'> & React.HTMLProps<HTMLDivElement>) =>
+        <div className={classes.backdrop} {...props} />);
diff --git a/src/views-components/user-dialog/manage-dialog.tsx b/src/views-components/user-dialog/manage-dialog.tsx
new file mode 100644 (file)
index 0000000..d70d35f
--- /dev/null
@@ -0,0 +1,77 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from "@material-ui/core";
+import { WithDialogProps } from "~/store/dialog/with-dialog";
+import { withDialog } from '~/store/dialog/with-dialog';
+import { WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { compose, Dispatch } from "redux";
+import { USER_MANAGEMENT_DIALOG, openSetupShellAccount, loginAs } from "~/store/users/users-actions";
+import { UserResource } from "~/models/user";
+import { connect } from "react-redux";
+
+type CssRules = 'spacing';
+
+const styles = withStyles<CssRules>((theme: ArvadosTheme) => ({
+    spacing: {
+        paddingBottom: theme.spacing.unit * 2,
+        paddingTop: theme.spacing.unit * 2,
+    }
+}));
+
+interface UserManageDataProps {
+    data: any;
+}
+
+interface UserManageActionProps {
+    openSetupShellAccount: (uuid: string) => void;
+    loginAs: (uuid: string) => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    openSetupShellAccount: (uuid: string) => dispatch<any>(openSetupShellAccount(uuid)),
+    loginAs: (uuid: string) => dispatch<any>(loginAs(uuid))
+});
+
+type UserManageProps = UserManageDataProps & UserManageActionProps & WithStyles<CssRules>;
+
+export const UserManageDialog = compose(
+    connect(null, mapDispatchToProps),
+    withDialog(USER_MANAGEMENT_DIALOG),
+    styles)(
+        (props: WithDialogProps<UserManageProps> & UserManageProps) =>
+            <Dialog open={props.open}
+                onClose={props.closeDialog}
+                fullWidth
+                maxWidth="md">
+                {props.data.user &&
+                    <span>
+                        <DialogTitle>{`Manage - ${props.data.user.firstName} ${props.data.user.lastName}`}</DialogTitle>
+                        <DialogContent>
+                            <Typography variant="body2" className={props.classes.spacing}>
+                                As an admin, you can log in as this user. When you’ve finished, you will need to log out and log in again with your own account.
+                    </Typography>
+                            <Button variant="contained" color="primary" onClick={() => props.loginAs(props.data.client.uuid)}>
+                                {`LOG IN AS ${props.data.user.firstName} ${props.data.user.lastName}`}
+                            </Button>
+                            <Typography variant="body2" className={props.classes.spacing}>
+                                As an admin, you can setup a shell account for this user. The login name is automatically generated from the user's e-mail address.
+                    </Typography>
+                            <Button variant="contained" color="primary" onClick={() => props.openSetupShellAccount(props.data.uuid)}>
+                                {`SETUP SHELL ACCOUNT FOR ${props.data.user.firstName} ${props.data.user.lastName}`}
+                            </Button>
+                        </DialogContent></span>}
+
+                <DialogActions>
+                    <Button
+                        variant='flat'
+                        color='primary'
+                        onClick={props.closeDialog}>
+                        Close
+                </Button>
+                </DialogActions>
+            </Dialog>
+    );
index 52921b30186f6dc412f9b9b1331e30e357b0d128..4a8cb9d5b81a5fd836463f57ab21e7256d10721c 100644 (file)
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { 
-    StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, 
-    Table, TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton
+import {
+    StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Tooltip, IconButton
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { MoreOptionsIcon, HelpIcon } from '~/components/icon/icon';
-import { ApiClientAuthorization } from '~/models/api-client-authorization';
-import { formatDate } from '~/common/formatters';
+import { HelpIcon, ShareMeIcon } from '~/components/icon/icon';
+import { createTree } from '~/models/tree';
+import { DataColumns } from '~/components/data-table/data-table';
+import { SortDirection } from '~/components/data-table/data-column';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { API_CLIENT_AUTHORIZATION_PANEL_ID } from '../../store/api-client-authorizations/api-client-authorizations-actions';
+import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
+import { ResourcesState } from '~/store/resources/resources';
+import {
+    CommonUuid, TokenApiClientId, TokenApiToken, TokenCreatedByIpAddress, TokenDefaultOwnerUuid, TokenExpiresAt,
+    TokenLastUsedAt, TokenLastUsedByIpAddress, TokenScopes, TokenUserId
+} from '~/views-components/data-explorer/renderers';
 
-type CssRules = 'root' | 'tableRow' | 'helpIconGrid' | 'tableGrid';
+type CssRules = 'card' | 'cardContent' | 'helpIconGrid';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    root: {
+    card: {
         width: '100%',
         overflow: 'auto'
     },
+    cardContent: {
+        padding: 0,
+        '&:last-child': {
+            paddingBottom: 0
+        }
+    },
     helpIconGrid: {
         textAlign: 'right'
+    }
+});
+
+
+export enum ApiClientAuthorizationPanelColumnNames {
+    UUID = 'UUID',
+    API_CLIENT_ID = 'API Client ID',
+    API_TOKEN = 'API Token',
+    CREATED_BY_IP_ADDRESS = 'Created by IP address',
+    DEFAULT_OWNER_UUID = 'Default owner',
+    EXPIRES_AT = 'Expires at',
+    LAST_USED_AT = 'Last used at',
+    LAST_USED_BY_IP_ADDRESS = 'Last used by IP address',
+    SCOPES = 'Scopes',
+    USER_ID = 'User ID'
+}
+
+export const apiClientAuthorizationPanelColumns: DataColumns<string> = [
+    {
+        name: ApiClientAuthorizationPanelColumnNames.UUID,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <CommonUuid uuid={uuid} />
     },
-    tableGrid: {
-        marginTop: theme.spacing.unit
+    {
+        name: ApiClientAuthorizationPanelColumnNames.API_CLIENT_ID,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenApiClientId uuid={uuid} />
     },
-    tableRow: {
-        '& td, th': {
-            whiteSpace: 'nowrap'
-        }
+    {
+        name: ApiClientAuthorizationPanelColumnNames.API_TOKEN,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenApiToken uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.CREATED_BY_IP_ADDRESS,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenCreatedByIpAddress uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.DEFAULT_OWNER_UUID,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenDefaultOwnerUuid uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.EXPIRES_AT,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenExpiresAt uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.LAST_USED_AT,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenLastUsedAt uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.LAST_USED_BY_IP_ADDRESS,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenLastUsedByIpAddress uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.SCOPES,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenScopes uuid={uuid} />
+    },
+    {
+        name: ApiClientAuthorizationPanelColumnNames.USER_ID,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <TokenUserId uuid={uuid} />
     }
-});
+];
+
+const DEFAULT_MESSAGE = 'Your api client authorization list is empty.';
 
 export interface ApiClientAuthorizationPanelRootActionProps {
-    openRowOptions: (event: React.MouseEvent<HTMLElement>, keepService: ApiClientAuthorization) => void;
+    onItemClick: (item: string) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
+    onItemDoubleClick: (item: string) => void;
     openHelpDialog: () => void;
 }
 
 export interface ApiClientAuthorizationPanelRootDataProps {
-    apiClientAuthorizations: ApiClientAuthorization[];
-    hasApiClientAuthorizations: boolean;
+    resources: ResourcesState;
 }
 
-type ApiClientAuthorizationPanelRootProps = ApiClientAuthorizationPanelRootActionProps 
+type ApiClientAuthorizationPanelRootProps = ApiClientAuthorizationPanelRootActionProps
     & ApiClientAuthorizationPanelRootDataProps & WithStyles<CssRules>;
 
 export const ApiClientAuthorizationPanelRoot = withStyles(styles)(
-    ({ classes, hasApiClientAuthorizations, apiClientAuthorizations, openRowOptions, openHelpDialog }: ApiClientAuthorizationPanelRootProps) =>
-        <Card className={classes.root}>
-            <CardContent>
-                {hasApiClientAuthorizations && <Grid container direction="row" justify="flex-end">
+    ({ classes, onItemDoubleClick, onItemClick, onContextMenu, openHelpDialog }: ApiClientAuthorizationPanelRootProps) =>
+        <Card className={classes.card}>
+            <CardContent className={classes.cardContent}>
+                <Grid container direction="row" justify="flex-end">
                     <Grid item xs={12} className={classes.helpIconGrid}>
                         <Tooltip title="Api token - help">
                             <IconButton onClick={openHelpDialog}>
@@ -58,47 +155,21 @@ export const ApiClientAuthorizationPanelRoot = withStyles(styles)(
                         </Tooltip>
                     </Grid>
                     <Grid item xs={12}>
-                        <Table>
-                            <TableHead>
-                                <TableRow className={classes.tableRow}>
-                                    <TableCell>UUID</TableCell>
-                                    <TableCell>API Client ID</TableCell>
-                                    <TableCell>API Token</TableCell>
-                                    <TableCell>Created by IP address</TableCell>
-                                    <TableCell>Default owner</TableCell>
-                                    <TableCell>Expires at</TableCell>
-                                    <TableCell>Last used at</TableCell>
-                                    <TableCell>Last used by IP address</TableCell>
-                                    <TableCell>Scopes</TableCell>
-                                    <TableCell>User ID</TableCell>
-                                    <TableCell />
-                                </TableRow>
-                            </TableHead>
-                            <TableBody>
-                                {apiClientAuthorizations.map((apiClientAuthorizatio, index) =>
-                                    <TableRow key={index} className={classes.tableRow}>
-                                        <TableCell>{apiClientAuthorizatio.uuid}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.apiClientId}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.apiToken}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.createdByIpAddress || '(none)'}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.defaultOwnerUuid || '(none)'}</TableCell>
-                                        <TableCell>{formatDate(apiClientAuthorizatio.expiresAt) || '(none)'}</TableCell>
-                                        <TableCell>{formatDate(apiClientAuthorizatio.lastUsedAt) || '(none)'}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.lastUsedByIpAddress || '(none)'}</TableCell>
-                                        <TableCell>{JSON.stringify(apiClientAuthorizatio.scopes)}</TableCell>
-                                        <TableCell>{apiClientAuthorizatio.userId}</TableCell>
-                                        <TableCell>
-                                            <Tooltip title="More options" disableFocusListener>
-                                                <IconButton onClick={event => openRowOptions(event, apiClientAuthorizatio)}>
-                                                    <MoreOptionsIcon />
-                                                </IconButton>
-                                            </Tooltip>
-                                        </TableCell>
-                                    </TableRow>)}
-                            </TableBody>
-                        </Table>
+                        <DataExplorer
+                            id={API_CLIENT_AUTHORIZATION_PANEL_ID}
+                            onRowClick={onItemClick}
+                            onRowDoubleClick={onItemDoubleClick}
+                            onContextMenu={onContextMenu}
+                            contextMenuColumn={true}
+                            hideColumnSelector
+                            hideSearchInput
+                            dataTableDefaultView={
+                                <DataTableDefaultView
+                                    icon={ShareMeIcon}
+                                    messages={[DEFAULT_MESSAGE]} />
+                            } />
                     </Grid>
-                </Grid>}
+                </Grid>
             </CardContent>
         </Card>
 );
\ No newline at end of file
index 75b79abff6270fa2eb9aff5695495f1e6f3a9f3f..b16e507646f0c8c22238efc8ccc773aba3d2254c 100644 (file)
@@ -15,15 +15,16 @@ import { openApiClientAuthorizationsHelpDialog } from '~/store/api-client-author
 
 const mapStateToProps = (state: RootState): ApiClientAuthorizationPanelRootDataProps => {
     return {
-        apiClientAuthorizations: state.apiClientAuthorizations,
-        hasApiClientAuthorizations: state.apiClientAuthorizations.length > 0
+        resources: state.resources
     };
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ApiClientAuthorizationPanelRootActionProps => ({
-    openRowOptions: (event, apiClientAuthorization) => {
+    onContextMenu: (event, apiClientAuthorization) => {
         dispatch<any>(openApiClientAuthorizationContextMenu(event, apiClientAuthorization));
     },
+    onItemClick: (resourceUuid: string) => { return; },
+    onItemDoubleClick: uuid => { return; },
     openHelpDialog: () => {
         dispatch<any>(openApiClientAuthorizationsHelpDialog());
     }
index 88a0e5513ce9c68936eaecca5c78c3d52a0dbf2b..530b75378da314cf8f11dc64f467e6268b8ff936 100644 (file)
@@ -11,7 +11,7 @@ import { DataColumns, DataTableFetchMode } from '~/components/data-table/data-ta
 import { SortDirection } from '~/components/data-table/data-column';
 import { createTree } from '~/models/tree';
 import {
-    ComputeNodeUuid, ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
+    ComputeUuid, ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
     ComputeNodeFirstPingAt, ComputeNodeLastPingAt, ComputeNodeIpAddress
 } from '~/views-components/data-explorer/renderers';
 import { ResourcesState } from '~/store/resources/resources';
@@ -41,7 +41,7 @@ export const computeNodePanelColumns: DataColumns<string> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <ComputeNodeUuid uuid={uuid} />
+        render: uuid => <CommonUuid uuid={uuid} />
     },
     {
         name: ComputeNodePanelColumnNames.DOMAIN,
index e6a2763553a9548e8f3f897cad9167efcafb9248..a0b29fb4b41f6764285c08378a8ee1649d3efd20 100644 (file)
@@ -114,7 +114,7 @@ export const MyAccountPanelRoot = withStyles(styles)(
                                 />
                             </Grid>
                             <Grid item className={classes.gridItem}>
-                                <InputLabel className={classes.label} htmlFor="prefs.profile.role">Organization</InputLabel>
+                                <InputLabel className={classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
                                 <Field
                                     id="prefs.profile.role"
                                     name="prefs.profile.role"
index b152896ff0d98a5d6592260133a06ff91e35fed8..c42036edf17ea5c0fae129a72fef980b4fa1a8d7 100644 (file)
@@ -28,6 +28,7 @@ import { compose, Dispatch } from 'redux';
 import { UserResource } from '~/models/user';
 import { ShareMeIcon, AddIcon } from '~/components/icon/icon';
 import { USERS_PANEL_ID, openUserCreateDialog } from '~/store/users/users-actions';
+import { noop } from 'lodash';
 
 type UserPanelRules = "button";
 
@@ -165,8 +166,8 @@ export const UserPanel = compose(
                         <span>
                             <DataExplorer
                                 id={USERS_PANEL_ID}
-                                onRowClick={this.handleRowClick}
-                                onRowDoubleClick={this.handleRowDoubleClick}
+                                onRowClick={noop}
+                                onRowDoubleClick={noop}
                                 onContextMenu={this.handleContextMenu}
                                 contextMenuColumn={true}
                                 hideColumnSelector
@@ -205,13 +206,5 @@ export const UserPanel = compose(
                     });
                 }
             }
-
-            handleRowDoubleClick = (uuid: string) => {
-                this.props.handleRowDoubleClick(uuid);
-            }
-
-            handleRowClick = () => {
-                return;
-            }
         }
     );
index 90b2dad0197215578d8020b1da153d7d38c9e88e..cca374d6738e9645f5183871252a20d66311a979 100644 (file)
@@ -80,6 +80,8 @@ import { UserPanel } from '~/views/user-panel/user-panel';
 import { UserAttributesDialog } from '~/views-components/user-dialog/attributes-dialog';
 import { CreateUserDialog } from '~/views-components/dialog-forms/create-user-dialog';
 import { HelpApiClientAuthorizationDialog } from '~/views-components/api-client-authorizations-dialog/help-dialog';
+import { UserManageDialog } from '~/views-components/user-dialog/manage-dialog';
+import { SetupShellAccountDialog } from '~/views-components/dialog-forms/setup-shell-account-dialog';
 import { GroupsPanel } from '~/views/groups-panel/groups-panel';
 import { CreateGroupDialog } from '~/views-components/dialog-forms/create-group-dialog';
 import { RemoveGroupDialog } from '~/views-components/groups-dialog/remove-dialog';
@@ -225,12 +227,14 @@ export const WorkbenchPanel =
             <RepositoryAttributesDialog />
             <RepositoriesSampleGitDialog />
             <RichTextEditorDialog />
+            <SetupShellAccountDialog />
             <SharingDialog />
             <Snackbar />
             <UpdateCollectionDialog />
             <UpdateProcessDialog />
             <UpdateProjectDialog />
             <UserAttributesDialog />
+            <UserManageDialog />
             <VirtualMachineAttributesDialog />
         </Grid>
     );