paperKey
} = 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
columns={columns}
onColumnToggle={onColumnToggle} />}
</Grid>
- </Toolbar>
+ </Toolbar>}
<DataTable
columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
items={items}
name: "Actions",
selected: true,
configurable: false,
- sortDirection: SortDirection.NONE,
filters: createTree(),
key: "context-actions",
render: this.renderContextMenuTrigger
//
// 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;
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");
});
});
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: '',
// 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";
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 {
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 } }));
};
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;
--- /dev/null
+// 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
+ });
+++ /dev/null
-// 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
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);
export const loadSiteManagerPanel = () =>
async (dispatch: Dispatch<any>) => {
try {
- dispatch(setBreadcrumbs([{ label: 'Site Manager'}]));
+ dispatch(setBreadcrumbs([{ label: 'Site Manager' }]));
dispatch(validateSessions());
} catch (e) {
return;
};
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
}));
// 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';
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';
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' &&
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),
groupDetailsPanelMiddleware,
linkPanelMiddleware,
computeNodeMiddleware,
+ apiClientAuthorizationMiddlewareService
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
searchBar: searchBarReducer,
virtualMachines: virtualMachinesReducer,
repositories: repositoriesReducer,
- keepServices: keepServicesReducer,
- apiClientAuthorizations: apiClientAuthorizationsReducer
+ keepServices: keepServicesReducer
});
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;
}
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();
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';
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);
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",
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
}
-},
-{
+}, {
name: "Manage",
icon: UserPanelIcon,
execute: (dispatch, { uuid }) => {
- dispatch<any>(openAdvancedTabDialog(uuid));
+ dispatch<any>(openUserManagement(uuid));
}
}]];
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>;
};
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 }) =>
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>;
const UserAddFields = (props: DialogUserProps) => <span>
<UserEmailField />
- <UserIdentityUrlField />
<UserVirtualMachineField data={props.data}/>
<UserGroupsVirtualMachineField />
</span>;
--- /dev/null
+// 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 />
+ </>;
+
+
+
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>
--- /dev/null
+// 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>
+ );
// 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}>
</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
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());
}
import { SortDirection } from '~/components/data-table/data-column';
import { createTree } from '~/models/tree';
import {
- ComputeNodeUuid, ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
+ CommonUuid, ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
ComputeNodeFirstPingAt, ComputeNodeLastPingAt, ComputeNodeIpAddress
} from '~/views-components/data-explorer/renderers';
import { ResourcesState } from '~/store/resources/resources';
configurable: true,
sortDirection: SortDirection.NONE,
filters: createTree(),
- render: uuid => <ComputeNodeUuid uuid={uuid} />
+ render: uuid => <CommonUuid uuid={uuid} />
},
{
name: ComputeNodePanelColumnNames.DOMAIN,
/>
</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"
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";
<span>
<DataExplorer
id={USERS_PANEL_ID}
- onRowClick={this.handleRowClick}
- onRowDoubleClick={this.handleRowDoubleClick}
+ onRowClick={noop}
+ onRowDoubleClick={noop}
onContextMenu={this.handleContextMenu}
contextMenuColumn={true}
hideColumnSelector
});
}
}
-
- handleRowDoubleClick = (uuid: string) => {
- this.props.handleRowDoubleClick(uuid);
- }
-
- handleRowClick = () => {
- return;
- }
}
);
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';
<RepositoryAttributesDialog />
<RepositoriesSampleGitDialog />
<RichTextEditorDialog />
+ <SetupShellAccountDialog />
<SharingDialog />
<Snackbar />
<UpdateCollectionDialog />
<UpdateProcessDialog />
<UpdateProjectDialog />
<UserAttributesDialog />
+ <UserManageDialog />
<VirtualMachineAttributesDialog />
</Grid>
);