From d19d853e83383a6b75f638be99472aa626a05524 Mon Sep 17 00:00:00 2001 From: Janicki Artur Date: Thu, 6 Dec 2018 16:16:48 +0100 Subject: [PATCH] init api token, add model and service, create dialogs and panel Feature #14500_admin_api_tokens Arvados-DCO-1.1-Signed-off-by: Janicki Artur --- src/index.tsx | 2 + src/models/api-client-authorization.ts | 19 +++ src/models/resource.ts | 4 + src/routes/route-change-handlers.ts | 3 + src/routes/routes.ts | 8 +- .../api-client-authorization-service.ts | 14 ++ .../common-service/common-resource-service.ts | 126 +---------------- src/services/common-service/common-service.ts | 131 ++++++++++++++++++ .../favorite-service/favorite-service.ts | 2 +- src/services/groups-service/groups-service.ts | 3 +- .../permission-service/permission-service.ts | 3 +- .../project-service/project-service.ts | 2 +- src/services/services.ts | 3 + src/store/advanced-tab/advanced-tab.ts | 52 ++++++- .../api-client-authorizations-actions.ts | 72 ++++++++++ .../api-client-authorizations-reducer.ts | 22 +++ .../context-menu/context-menu-actions.ts | 13 ++ .../data-explorer-middleware-service.ts | 2 +- src/store/navigation/navigation-action.ts | 4 +- .../project-panel-middleware-service.ts | 2 +- .../search-results-middleware-service.ts | 2 +- .../shared-with-me-middleware-service.ts | 2 +- src/store/store.ts | 4 +- .../virtual-machines-actions.ts | 2 +- .../virtual-machines-reducer.ts | 2 +- src/store/workbench/workbench-actions.ts | 6 + .../workflow-middleware-service.ts | 2 +- .../attributes-dialog.tsx | 55 ++++++++ .../remove-dialog.tsx | 20 +++ .../api-client-authorization-action-set.ts | 31 +++++ .../context-menu/context-menu.tsx | 1 + .../main-app-bar/account-menu.tsx | 6 +- .../api-client-authorization-panel-root.tsx | 89 ++++++++++++ .../api-client-authorization-panel.tsx | 28 ++++ .../compute-node-panel-root.tsx | 2 +- .../virtual-machine-panel.tsx | 2 +- src/views/workbench/workbench.tsx | 6 + 37 files changed, 604 insertions(+), 143 deletions(-) create mode 100644 src/models/api-client-authorization.ts create mode 100644 src/services/api-client-authorization-service/api-client-authorization-service.ts create mode 100644 src/services/common-service/common-service.ts create mode 100644 src/store/api-client-authorizations/api-client-authorizations-actions.ts create mode 100644 src/store/api-client-authorizations/api-client-authorizations-reducer.ts create mode 100644 src/views-components/api-client-authorizations-dialog/attributes-dialog.tsx create mode 100644 src/views-components/api-client-authorizations-dialog/remove-dialog.tsx create mode 100644 src/views-components/context-menu/action-sets/api-client-authorization-action-set.ts create mode 100644 src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx create mode 100644 src/views/api-client-authorization-panel/api-client-authorization-panel.tsx diff --git a/src/index.tsx b/src/index.tsx index fbd6c9a8..c33ef7c1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -54,6 +54,7 @@ import { keepServiceActionSet } from '~/views-components/context-menu/action-set import { loadVocabulary } from '~/store/vocabulary/vocabulary-actions'; import { virtualMachineActionSet } from '~/views-components/context-menu/action-sets/virtual-machine-action-set'; import { computeNodeActionSet } from '~/views-components/context-menu/action-sets/compute-node-action-set'; +import { apiClientAuthorizationActionSet } from '~/views-components/context-menu/action-sets/api-client-authorization-action-set'; console.log(`Starting arvados [${getBuildInfo()}]`); @@ -75,6 +76,7 @@ addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet); addMenuActionSet(ContextMenuKind.VIRTUAL_MACHINE, virtualMachineActionSet); addMenuActionSet(ContextMenuKind.KEEP_SERVICE, keepServiceActionSet); addMenuActionSet(ContextMenuKind.NODE, computeNodeActionSet); +addMenuActionSet(ContextMenuKind.API_CLIENT_AUTHORIZATION, apiClientAuthorizationActionSet); fetchConfig() .then(({ config, apiHost }) => { diff --git a/src/models/api-client-authorization.ts b/src/models/api-client-authorization.ts new file mode 100644 index 00000000..aff50be6 --- /dev/null +++ b/src/models/api-client-authorization.ts @@ -0,0 +1,19 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +export interface ApiClientAuthorization { + uuid: string; + apiToken: string; + apiClientId: number; + userId: number; + createdByIpAddress: string; + lastUsedByIpAddress: string; + lastUsedAt: string; + expiresAt: string; + createdAt: string; + updatedAt: string; + ownerUuid: string; + defaultOwnerUuid: string; + scopes: string[]; +} \ No newline at end of file diff --git a/src/models/resource.ts b/src/models/resource.ts index 4d2d92e0..eddcd5a0 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -21,6 +21,7 @@ export interface TrashableResource extends Resource { } export enum ResourceKind { + API_CLIENT_AUTHORIZATION = "arvados#apiClientAuthorization", COLLECTION = "arvados#collection", CONTAINER = "arvados#container", CONTAINER_REQUEST = "arvados#containerRequest", @@ -39,6 +40,7 @@ export enum ResourceKind { } export enum ResourceObjectType { + API_CLIENT_AUTHORIZATION = 'gj3su', COLLECTION = '4zz18', CONTAINER = 'dz642', CONTAINER_REQUEST = 'xvhdp', @@ -93,6 +95,8 @@ export const extractUuidKind = (uuid: string = '') => { return ResourceKind.KEEP_SERVICE; case ResourceObjectType.NODE: return ResourceKind.NODE; + case ResourceObjectType.API_CLIENT_AUTHORIZATION: + return ResourceKind.API_CLIENT_AUTHORIZATION; default: return undefined; } diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index f2304aca..1a88ff20 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -31,6 +31,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { const sshKeysMatch = Routes.matchSshKeysRoute(pathname); const keepServicesMatch = Routes.matchKeepServicesRoute(pathname); const computeNodesMatch = Routes.matchComputeNodesRoute(pathname); + const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname); if (projectMatch) { store.dispatch(WorkbenchActions.loadProject(projectMatch.params.id)); @@ -64,5 +65,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { store.dispatch(WorkbenchActions.loadKeepServices); } else if (computeNodesMatch) { store.dispatch(WorkbenchActions.loadComputeNodes); + } else if (apiClientAuthorizationsMatch) { + store.dispatch(WorkbenchActions.loadApiClientAuthorizations); } }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 8f8fa06b..1fda25d0 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -24,7 +24,8 @@ export const Routes = { SEARCH_RESULTS: '/search-results', SSH_KEYS: `/ssh-keys`, KEEP_SERVICES: `/keep-services`, - COMPUTE_NODES: `/nodes` + COMPUTE_NODES: `/nodes`, + API_CLIENT_AUTHORIZATIONS: `/api_client_authorizations` }; export const getResourceUrl = (uuid: string) => { @@ -95,4 +96,7 @@ export const matchKeepServicesRoute = (route: string) => matchPath(route, { path: Routes.KEEP_SERVICES }); export const matchComputeNodesRoute = (route: string) => - matchPath(route, { path: Routes.COMPUTE_NODES }); \ No newline at end of file + matchPath(route, { path: Routes.COMPUTE_NODES }); + +export const matchApiClientAuthorizationsRoute = (route: string) => + matchPath(route, { path: Routes.API_CLIENT_AUTHORIZATIONS }); \ No newline at end of file diff --git a/src/services/api-client-authorization-service/api-client-authorization-service.ts b/src/services/api-client-authorization-service/api-client-authorization-service.ts new file mode 100644 index 00000000..3bf4ae8a --- /dev/null +++ b/src/services/api-client-authorization-service/api-client-authorization-service.ts @@ -0,0 +1,14 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { AxiosInstance } from "axios"; +import { ApiActions } from '~/services/api/api-actions'; +import { ApiClientAuthorization } from '~/models/api-client-authorization'; +import { CommonService } from '~/services/common-service/common-service'; + +export class ApiClientAuthorizationService extends CommonService { + constructor(serverApi: AxiosInstance, actions: ApiActions) { + super(serverApi, "api_client_authorizations", actions); + } +} \ No newline at end of file diff --git a/src/services/common-service/common-resource-service.ts b/src/services/common-service/common-resource-service.ts index 6114c560..471c32fa 100644 --- a/src/services/common-service/common-resource-service.ts +++ b/src/services/common-service/common-resource-service.ts @@ -3,33 +3,10 @@ // SPDX-License-Identifier: AGPL-3.0 import * as _ from "lodash"; -import { AxiosInstance, AxiosPromise } from "axios"; +import { AxiosInstance } from "axios"; import { Resource } from "src/models/resource"; -import * as uuid from "uuid/v4"; import { ApiActions } from "~/services/api/api-actions"; - -export interface ListArguments { - limit?: number; - offset?: number; - filters?: string; - order?: string; - select?: string[]; - distinct?: boolean; - count?: string; -} - -export interface ListResults { - kind: string; - offset: number; - limit: number; - items: T[]; - itemsAvailable: number; -} - -export interface Errors { - errors: string[]; - errorToken: string; -} +import { CommonService } from "~/services/common-service/common-service"; export enum CommonResourceServiceError { UNIQUE_VIOLATION = 'UniqueViolation', @@ -40,105 +17,12 @@ export enum CommonResourceServiceError { NONE = 'None' } -export class CommonResourceService { - - static mapResponseKeys = (response: { data: any }) => - CommonResourceService.mapKeys(_.camelCase)(response.data) - - static mapKeys = (mapFn: (key: string) => string) => - (value: any): any => { - switch (true) { - case _.isPlainObject(value): - return Object - .keys(value) - .map(key => [key, mapFn(key)]) - .reduce((newValue, [key, newKey]) => ({ - ...newValue, - [newKey]: CommonResourceService.mapKeys(mapFn)(value[key]) - }), {}); - case _.isArray(value): - return value.map(CommonResourceService.mapKeys(mapFn)); - default: - return value; - } - } - - static defaultResponse(promise: AxiosPromise, actions: ApiActions, mapKeys = true): Promise { - const reqId = uuid(); - actions.progressFn(reqId, true); - return promise - .then(data => { - actions.progressFn(reqId, false); - return data; - }) - .then((response: { data: any }) => { - return mapKeys ? CommonResourceService.mapResponseKeys(response) : response.data; - }) - .catch(({ response }) => { - actions.progressFn(reqId, false); - const errors = CommonResourceService.mapResponseKeys(response) as Errors; - actions.errorFn(reqId, errors); - throw errors; - }); - } - - protected serverApi: AxiosInstance; - protected resourceType: string; - protected actions: ApiActions; +export class CommonResourceService extends CommonService { constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) { - this.serverApi = serverApi; - this.resourceType = '/' + resourceType + '/'; - this.actions = actions; - } - - create(data?: Partial) { - return CommonResourceService.defaultResponse( - this.serverApi - .post(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)), - this.actions - ); - } - - delete(uuid: string): Promise { - return CommonResourceService.defaultResponse( - this.serverApi - .delete(this.resourceType + uuid), - this.actions - ); - } - - get(uuid: string) { - return CommonResourceService.defaultResponse( - this.serverApi - .get(this.resourceType + uuid), - this.actions - ); - } - - list(args: ListArguments = {}): Promise> { - const { filters, order, ...other } = args; - const params = { - ...other, - filters: filters ? `[${filters}]` : undefined, - order: order ? order : undefined - }; - return CommonResourceService.defaultResponse( - this.serverApi - .get(this.resourceType, { - params: CommonResourceService.mapKeys(_.snakeCase)(params) - }), - this.actions - ); - } - - update(uuid: string, data: Partial) { - return CommonResourceService.defaultResponse( - this.serverApi - .put(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)), - this.actions - ); + super(serverApi, resourceType, actions); } + } export const getCommonResourceServiceError = (errorResponse: any) => { diff --git a/src/services/common-service/common-service.ts b/src/services/common-service/common-service.ts new file mode 100644 index 00000000..b301a727 --- /dev/null +++ b/src/services/common-service/common-service.ts @@ -0,0 +1,131 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as _ from "lodash"; +import { AxiosInstance, AxiosPromise } from "axios"; +import * as uuid from "uuid/v4"; +import { ApiActions } from "~/services/api/api-actions"; + +interface Errors { + errors: string[]; + errorToken: string; +} + +export interface ListArguments { + limit?: number; + offset?: number; + filters?: string; + order?: string; + select?: string[]; + distinct?: boolean; + count?: string; +} + +export interface ListResults { + kind: string; + offset: number; + limit: number; + items: T[]; + itemsAvailable: number; +} + +export class CommonService { + protected serverApi: AxiosInstance; + protected resourceType: string; + protected actions: ApiActions; + + constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) { + this.serverApi = serverApi; + this.resourceType = '/' + resourceType + '/'; + this.actions = actions; + } + + static mapResponseKeys = (response: { data: any }) => + CommonService.mapKeys(_.camelCase)(response.data) + + static mapKeys = (mapFn: (key: string) => string) => + (value: any): any => { + switch (true) { + case _.isPlainObject(value): + return Object + .keys(value) + .map(key => [key, mapFn(key)]) + .reduce((newValue, [key, newKey]) => ({ + ...newValue, + [newKey]: CommonService.mapKeys(mapFn)(value[key]) + }), {}); + case _.isArray(value): + return value.map(CommonService.mapKeys(mapFn)); + default: + return value; + } + } + + static defaultResponse(promise: AxiosPromise, actions: ApiActions, mapKeys = true): Promise { + const reqId = uuid(); + actions.progressFn(reqId, true); + return promise + .then(data => { + actions.progressFn(reqId, false); + return data; + }) + .then((response: { data: any }) => { + return mapKeys ? CommonService.mapResponseKeys(response) : response.data; + }) + .catch(({ response }) => { + actions.progressFn(reqId, false); + const errors = CommonService.mapResponseKeys(response) as Errors; + actions.errorFn(reqId, errors); + throw errors; + }); + } + + create(data?: Partial) { + return CommonService.defaultResponse( + this.serverApi + .post(this.resourceType, data && CommonService.mapKeys(_.snakeCase)(data)), + this.actions + ); + } + + delete(uuid: string): Promise { + return CommonService.defaultResponse( + this.serverApi + .delete(this.resourceType + uuid), + this.actions + ); + } + + get(uuid: string) { + return CommonService.defaultResponse( + this.serverApi + .get(this.resourceType + uuid), + this.actions + ); + } + + list(args: ListArguments = {}): Promise> { + const { filters, order, ...other } = args; + const params = { + ...other, + filters: filters ? `[${filters}]` : undefined, + order: order ? order : undefined + }; + return CommonService.defaultResponse( + this.serverApi + .get(this.resourceType, { + params: CommonService.mapKeys(_.snakeCase)(params) + }), + this.actions + ); + } + + update(uuid: string, data: Partial) { + return CommonService.defaultResponse( + this.serverApi + .put(this.resourceType + uuid, data && CommonService.mapKeys(_.snakeCase)(data)), + this.actions + ); + } +} \ No newline at end of file diff --git a/src/services/favorite-service/favorite-service.ts b/src/services/favorite-service/favorite-service.ts index 92b0713d..c41b2b99 100644 --- a/src/services/favorite-service/favorite-service.ts +++ b/src/services/favorite-service/favorite-service.ts @@ -6,7 +6,7 @@ import { LinkService } from "../link-service/link-service"; import { GroupsService, GroupContentsResource } from "../groups-service/groups-service"; import { LinkClass } from "~/models/link"; import { FilterBuilder, joinFilters } from "~/services/api/filter-builder"; -import { ListResults } from "~/services/common-service/common-resource-service"; +import { ListResults } from '~/services/common-service/common-service'; export interface FavoriteListArguments { limit?: number; diff --git a/src/services/groups-service/groups-service.ts b/src/services/groups-service/groups-service.ts index d1e2eff2..d8b33f60 100644 --- a/src/services/groups-service/groups-service.ts +++ b/src/services/groups-service/groups-service.ts @@ -3,7 +3,8 @@ // SPDX-License-Identifier: AGPL-3.0 import * as _ from "lodash"; -import { CommonResourceService, ListResults, ListArguments } from '~/services/common-service/common-resource-service'; +import { CommonResourceService } from '~/services/common-service/common-resource-service'; +import { ListResults, ListArguments } from '~/services/common-service/common-service'; import { AxiosInstance } from "axios"; import { CollectionResource } from "~/models/collection"; import { ProjectResource } from "~/models/project"; diff --git a/src/services/permission-service/permission-service.ts b/src/services/permission-service/permission-service.ts index 4bb0fe09..763844e0 100644 --- a/src/services/permission-service/permission-service.ts +++ b/src/services/permission-service/permission-service.ts @@ -4,8 +4,9 @@ import { LinkService } from "~/services/link-service/link-service"; import { PermissionResource } from "~/models/permission"; -import { ListArguments, ListResults, CommonResourceService } from '~/services/common-service/common-resource-service'; +import { CommonResourceService } from '~/services/common-service/common-resource-service'; import { LinkClass } from '../../models/link'; +import { ListArguments, ListResults } from '~/services/common-service/common-service'; export class PermissionService extends LinkService { diff --git a/src/services/project-service/project-service.ts b/src/services/project-service/project-service.ts index d6003471..5c686aae 100644 --- a/src/services/project-service/project-service.ts +++ b/src/services/project-service/project-service.ts @@ -5,7 +5,7 @@ import { GroupsService } from "../groups-service/groups-service"; import { ProjectResource } from "~/models/project"; import { GroupClass } from "~/models/group"; -import { ListArguments } from "~/services/common-service/common-resource-service"; +import { ListArguments } from "~/services/common-service/common-service"; import { FilterBuilder, joinFilters } from "~/services/api/filter-builder"; import { TrashableResourceService } from '~/services/common-service/trashable-resource-service'; import { snakeCase } from 'lodash'; diff --git a/src/services/services.ts b/src/services/services.ts index d524405f..59fe2d47 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import Axios from "axios"; +import { ApiClientAuthorizationService } from '~/services/api-client-authorization-service/api-client-authorization-service'; import { AuthService } from "./auth-service/auth-service"; import { GroupsService } from "./groups-service/groups-service"; import { ProjectService } from "./project-service/project-service"; @@ -39,6 +40,7 @@ export const createServices = (config: Config, actions: ApiActions) => { const webdavClient = new WebDAV(); webdavClient.defaults.baseURL = config.keepWebServiceUrl; + const apiClientAuthorizationService = new ApiClientAuthorizationService(apiClient, actions); const authorizedKeysService = new AuthorizedKeysService(apiClient, actions); const containerRequestService = new ContainerRequestService(apiClient, actions); const containerService = new ContainerService(apiClient, actions); @@ -66,6 +68,7 @@ export const createServices = (config: Config, actions: ApiActions) => { return { ancestorsService, apiClient, + apiClientAuthorizationService, authService, authorizedKeysService, collectionFilesService, diff --git a/src/store/advanced-tab/advanced-tab.ts b/src/store/advanced-tab/advanced-tab.ts index 6b20f8b3..a77ffcca 100644 --- a/src/store/advanced-tab/advanced-tab.ts +++ b/src/store/advanced-tab/advanced-tab.ts @@ -14,14 +14,15 @@ import { CollectionResource } from '~/models/collection'; import { ProjectResource } from '~/models/project'; import { ServiceRepository } from '~/services/services'; import { FilterBuilder } from '~/services/api/filter-builder'; +import { ListResults } from '~/services/common-service/common-service'; import { RepositoryResource } from '~/models/repositories'; import { SshKeyResource } from '~/models/ssh-key'; import { VirtualMachinesResource } from '~/models/virtual-machines'; import { UserResource } from '~/models/user'; -import { ListResults } from '~/services/common-service/common-resource-service'; import { LinkResource } from '~/models/link'; import { KeepServiceResource } from '~/models/keep-services'; import { NodeResource } from '~/models/node'; +import { ApiClientAuthorization } from '~/models/api-client-authorization'; export const ADVANCED_TAB_DIALOG = 'advancedTabDialog'; @@ -74,7 +75,8 @@ enum ResourcePrefix { AUTORIZED_KEYS = 'authorized_keys', VIRTUAL_MACHINES = 'virtual_machines', KEEP_SERVICES = 'keep_services', - COMPUTE_NODES = 'nodes' + COMPUTE_NODES = 'nodes', + API_CLIENT_AUTHORIZATIONS = 'api_client_authorizations' } enum KeepServiceData { @@ -87,9 +89,14 @@ enum ComputeNodeData { PROPERTIES = 'properties' } -type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ComputeNodeData; +enum ApiClientAuthorizationsData { + API_CLIENT_AUTHORIZATION = 'api_client_authorization', + DEFAULT_OWNER_UUID = 'default_owner_uuid' +} + +type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ComputeNodeData | ApiClientAuthorizationsData; type AdvanceResourcePrefix = GroupContentsResourcePrefix | ResourcePrefix; -type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | NodeResource | undefined; +type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | NodeResource | ApiClientAuthorization | undefined; export const openAdvancedTabDialog = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { @@ -215,6 +222,21 @@ export const openAdvancedTabDialog = (uuid: string) => }); dispatch(initAdvancedTabDialog(advanceDataComputeNode)); break; + case ResourceKind.API_CLIENT_AUTHORIZATION: + const dataApiClientAuthorization = getState().apiClientAuthorizations.find(item => item.uuid === uuid); + const advanceDataApiClientAuthorization = advancedTabData({ + uuid, + metadata: '', + user: '', + apiResponseKind: apiClientAuthorizationApiResponse, + data: dataApiClientAuthorization, + resourceKind: ApiClientAuthorizationsData.API_CLIENT_AUTHORIZATION, + resourcePrefix: ResourcePrefix.API_CLIENT_AUTHORIZATIONS, + resourceKindProperty: ApiClientAuthorizationsData.DEFAULT_OWNER_UUID, + property: dataApiClientAuthorization!.defaultOwnerUuid + }); + dispatch(initAdvancedTabDialog(advanceDataApiClientAuthorization)); + break; default: dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR })); } @@ -487,5 +509,27 @@ const computeNodeApiResponse = (apiResponse: NodeResource) => { "properties": "${JSON.stringify(properties, null, 4)}", "info": "${JSON.stringify(info, null, 4)}"`; + return response; +}; + +const apiClientAuthorizationApiResponse = (apiResponse: ApiClientAuthorization) => { + const { + uuid, ownerUuid, apiToken, apiClientId, userId, createdByIpAddress, lastUsedByIpAddress, + lastUsedAt, expiresAt, defaultOwnerUuid, scopes, updatedAt, createdAt + } = apiResponse; + const response = `"uuid": "${uuid}", +"owner_uuid": "${ownerUuid}", +"api_token": "${stringify(apiToken)}", +"api_client_id": "${stringify(apiClientId)}", +"user_id": "${stringify(userId)}", +"created_by_ip_address": "${stringify(createdByIpAddress)}", +"last_used_by_ip_address": "${stringify(lastUsedByIpAddress)}", +"last_used_at": "${stringify(lastUsedAt)}", +"expires_at": "${stringify(expiresAt)}", +"created_at": "${stringify(createdAt)}", +"updated_at": "${stringify(updatedAt)}", +"default_owner_uuid": "${stringify(defaultOwnerUuid)}", +"scopes": "${JSON.stringify(scopes, null, 4)}"`; + return response; }; \ No newline at end of file diff --git a/src/store/api-client-authorizations/api-client-authorizations-actions.ts b/src/store/api-client-authorizations/api-client-authorizations-actions.ts new file mode 100644 index 00000000..5f52aa2a --- /dev/null +++ b/src/store/api-client-authorizations/api-client-authorizations-actions.ts @@ -0,0 +1,72 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// 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 { 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'; + +export const apiClientAuthorizationsActions = unionize({ + SET_API_CLIENT_AUTHORIZATIONS: ofType(), + REMOVE_API_CLIENT_AUTHORIZATION: ofType() +}); + +export type ApiClientAuthorizationsActions = UnionOf; + +export const API_CLIENT_AUTHORIZATION_REMOVE_DIALOG = 'apiClientAuthorizationRemoveDialog'; +export const API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG = 'apiClientAuthorizationAttributesDialog'; + +export const loadApiClientAuthorizationsPanel = () => + async (dispatch: Dispatch, 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)); + } catch (e) { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000 })); + return; + } + } else { + dispatch(navigateToRootProject); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000 })); + } + }; + +export const openApiClientAuthorizationAttributesDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + const apiClientAuthorization = getState().apiClientAuthorizations.find(node => node.uuid === uuid); + dispatch(dialogActions.OPEN_DIALOG({ id: API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG, data: { apiClientAuthorization } })); + }; + +export const openApiClientAuthorizationRemoveDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + dispatch(dialogActions.OPEN_DIALOG({ + id: API_CLIENT_AUTHORIZATION_REMOVE_DIALOG, + data: { + title: 'Remove api client authorization', + text: 'Are you sure you want to remove this api client authorization?', + confirmButtonLabel: 'Remove', + uuid + } + })); + }; + +export const removeApiClientAuthorization = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' })); + try { + await services.apiClientAuthorizationService.delete(uuid); + dispatch(apiClientAuthorizationsActions.REMOVE_API_CLIENT_AUTHORIZATION(uuid)); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Api client authorization has been successfully removed.', hideDuration: 2000 })); + } catch (e) { + return; + } + }; \ No newline at end of file diff --git a/src/store/api-client-authorizations/api-client-authorizations-reducer.ts b/src/store/api-client-authorizations/api-client-authorizations-reducer.ts new file mode 100644 index 00000000..7084dea7 --- /dev/null +++ b/src/store/api-client-authorizations/api-client-authorizations-reducer.ts @@ -0,0 +1,22 @@ +// 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 diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 65ddcff2..b7d6cb26 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -18,6 +18,7 @@ import { SshKeyResource } from '~/models/ssh-key'; import { VirtualMachinesResource } from '~/models/virtual-machines'; import { KeepServiceResource } from '~/models/keep-services'; import { NodeResource } from '~/models/node'; +import { ApiClientAuthorization } from '~/models/api-client-authorization'; export const contextMenuActions = unionize({ OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(), @@ -121,6 +122,18 @@ export const openComputeNodeContextMenu = (event: React.MouseEvent, })); }; +export const openApiClientAuthorizationContextMenu = + (event: React.MouseEvent, apiClientAuthorization: ApiClientAuthorization) => + (dispatch: Dispatch) => { + dispatch(openContextMenu(event, { + name: '', + uuid: apiClientAuthorization.uuid, + ownerUuid: apiClientAuthorization.ownerUuid, + kind: ResourceKind.API_CLIENT_AUTHORIZATION, + menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION + })); + }; + export const openRootProjectContextMenu = (event: React.MouseEvent, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => { const res = getResource(projectUuid)(getState().resources); diff --git a/src/store/data-explorer/data-explorer-middleware-service.ts b/src/store/data-explorer/data-explorer-middleware-service.ts index 80ab514c..82ba5b4b 100644 --- a/src/store/data-explorer/data-explorer-middleware-service.ts +++ b/src/store/data-explorer/data-explorer-middleware-service.ts @@ -6,7 +6,7 @@ import { Dispatch, MiddlewareAPI } from "redux"; import { RootState } from "../store"; import { DataColumns } from "~/components/data-table/data-table"; import { DataExplorer } from './data-explorer-reducer'; -import { ListResults } from '~/services/common-service/common-resource-service'; +import { ListResults } from '~/services/common-service/common-service'; import { createTree } from "~/models/tree"; import { DataTableFilters } from "~/components/data-table-filters/data-table-filters-tree"; diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index 50cfd88d..e1e8cef5 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -70,4 +70,6 @@ export const navigateToSshKeys= push(Routes.SSH_KEYS); export const navigateToKeepServices = push(Routes.KEEP_SERVICES); -export const navigateToComputeNodes = push(Routes.COMPUTE_NODES); \ No newline at end of file +export const navigateToComputeNodes = push(Routes.COMPUTE_NODES); + +export const navigateToApiClientAuthorizations = push(Routes.API_CLIENT_AUTHORIZATIONS); \ No newline at end of file diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts index 36672e99..f2cc8a92 100644 --- a/src/store/project-panel/project-panel-middleware-service.ts +++ b/src/store/project-panel/project-panel-middleware-service.ts @@ -25,7 +25,7 @@ import { getProperty } from "~/store/properties/properties"; import { snackbarActions, SnackbarKind } from '../snackbar/snackbar-actions'; import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts'; import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer'; -import { ListResults } from '~/services/common-service/common-resource-service'; +import { ListResults } from '~/services/common-service/common-service'; import { loadContainers } from '../processes/processes-actions'; import { ResourceKind } from '~/models/resource'; import { getResource } from "~/store/resources/resources"; diff --git a/src/store/search-results-panel/search-results-middleware-service.ts b/src/store/search-results-panel/search-results-middleware-service.ts index 1bd294f1..d8b4d7e7 100644 --- a/src/store/search-results-panel/search-results-middleware-service.ts +++ b/src/store/search-results-panel/search-results-middleware-service.ts @@ -13,7 +13,7 @@ import { SortDirection } from '~/components/data-table/data-column'; import { SearchResultsPanelColumnNames } from '~/views/search-results-panel/search-results-panel-view'; import { OrderDirection, OrderBuilder } from '~/services/api/order-builder'; import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service"; -import { ListResults } from '~/services/common-service/common-resource-service'; +import { ListResults } from '~/services/common-service/common-service'; import { searchResultsPanelActions } from '~/store/search-results-panel/search-results-panel-actions'; import { getFilters } from '~/store/search-bar/search-bar-actions'; import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer"; diff --git a/src/store/shared-with-me-panel/shared-with-me-middleware-service.ts b/src/store/shared-with-me-panel/shared-with-me-middleware-service.ts index c139b429..9e76d46d 100644 --- a/src/store/shared-with-me-panel/shared-with-me-middleware-service.ts +++ b/src/store/shared-with-me-panel/shared-with-me-middleware-service.ts @@ -12,7 +12,7 @@ import { updateResources } from '~/store/resources/resources-actions'; import { loadMissingProcessesInformation, getFilters } from '~/store/project-panel/project-panel-middleware-service'; import { snackbarActions } from '~/store/snackbar/snackbar-actions'; import { sharedWithMePanelActions } from './shared-with-me-panel-actions'; -import { ListResults } from '~/services/common-service/common-resource-service'; +import { ListResults } from '~/services/common-service/common-service'; import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service'; import { SortDirection } from '~/components/data-table/data-column'; import { OrderBuilder, OrderDirection } from '~/services/api/order-builder'; diff --git a/src/store/store.ts b/src/store/store.ts index 321a19b6..eef04750 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -47,6 +47,7 @@ import { virtualMachinesReducer } from "~/store/virtual-machines/virtual-machine import { repositoriesReducer } from '~/store/repositories/repositories-reducer'; import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer'; import { computeNodesReducer } from '~/store/compute-nodes/compute-nodes-reducer'; +import { apiClientAuthorizationsReducer } from '~/store/api-client-authorizations/api-client-authorizations-reducer'; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -119,5 +120,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({ virtualMachines: virtualMachinesReducer, repositories: repositoriesReducer, keepServices: keepServicesReducer, - computeNodes: computeNodesReducer + computeNodes: computeNodesReducer, + apiClientAuthorizations: apiClientAuthorizationsReducer }); diff --git a/src/store/virtual-machines/virtual-machines-actions.ts b/src/store/virtual-machines/virtual-machines-actions.ts index c95277b3..0aaa9050 100644 --- a/src/store/virtual-machines/virtual-machines-actions.ts +++ b/src/store/virtual-machines/virtual-machines-actions.ts @@ -11,7 +11,7 @@ import { formatDate } from "~/common/formatters"; import { unionize, ofType, UnionOf } from "~/common/unionize"; import { VirtualMachineLogins } from '~/models/virtual-machines'; import { FilterBuilder } from "~/services/api/filter-builder"; -import { ListResults } from "~/services/common-service/common-resource-service"; +import { ListResults } from "~/services/common-service/common-service"; import { dialogActions } from '~/store/dialog/dialog-actions'; import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions'; diff --git a/src/store/virtual-machines/virtual-machines-reducer.ts b/src/store/virtual-machines/virtual-machines-reducer.ts index 475ad752..3ee90d57 100644 --- a/src/store/virtual-machines/virtual-machines-reducer.ts +++ b/src/store/virtual-machines/virtual-machines-reducer.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import { virtualMachinesActions, VirtualMachineActions } from '~/store/virtual-machines/virtual-machines-actions'; -import { ListResults } from '~/services/common-service/common-resource-service'; +import { ListResults } from '~/services/common-service/common-service'; import { VirtualMachineLogins } from '~/models/virtual-machines'; interface VirtualMachines { diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index e3f96a9c..6e823be6 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -58,6 +58,7 @@ import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machi import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions'; import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions'; import { loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions'; +import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions'; export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen'; @@ -422,6 +423,11 @@ export const loadComputeNodes = handleFirstTimeLoad( await dispatch(loadComputeNodesPanel()); }); +export const loadApiClientAuthorizations = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + await dispatch(loadApiClientAuthorizationsPanel()); + }); + const finishLoadingProject = (project: GroupContentsResource | string) => async (dispatch: Dispatch) => { const uuid = typeof project === 'string' ? project : project.uuid; diff --git a/src/store/workflow-panel/workflow-middleware-service.ts b/src/store/workflow-panel/workflow-middleware-service.ts index fefcb325..2cd910bd 100644 --- a/src/store/workflow-panel/workflow-middleware-service.ts +++ b/src/store/workflow-panel/workflow-middleware-service.ts @@ -14,7 +14,7 @@ import { SortDirection } from '~/components/data-table/data-column'; import { WorkflowPanelColumnNames } from '~/views/workflow-panel/workflow-panel-view'; import { OrderDirection, OrderBuilder } from '~/services/api/order-builder'; import { WorkflowResource } from '~/models/workflow'; -import { ListResults } from '~/services/common-service/common-resource-service'; +import { ListResults } from '~/services/common-service/common-service'; import { workflowPanelActions } from './workflow-panel-actions'; import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer"; diff --git a/src/views-components/api-client-authorizations-dialog/attributes-dialog.tsx b/src/views-components/api-client-authorizations-dialog/attributes-dialog.tsx new file mode 100644 index 00000000..662c8808 --- /dev/null +++ b/src/views-components/api-client-authorizations-dialog/attributes-dialog.tsx @@ -0,0 +1,55 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { compose } from 'redux'; +import { + withStyles, Dialog, DialogTitle, DialogContent, DialogActions, + Button, StyleRulesCallback, WithStyles, Grid +} from '@material-ui/core'; +import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog"; +import { API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG } from '~/store/api-client-authorizations/api-client-authorizations-actions'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { ApiClientAuthorization } from '~/models/api-client-authorization'; + +type CssRules = 'root'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + fontSize: '0.875rem', + '& div:nth-child(odd)': { + textAlign: 'right', + color: theme.palette.grey["500"] + } + } +}); + +interface AttributesKeepServiceDialogDataProps { + apiClientAuthorization: ApiClientAuthorization; +} + +export const AttributesApiClientAuthorizationDialog = compose( + withDialog(API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG), + withStyles(styles))( + ({ open, closeDialog, data, classes }: WithDialogProps & WithStyles) => + + Attributes + + {data.apiClientAuthorization && + UUID + {data.apiClientAuthorization.uuid} + Owner uuid + {data.apiClientAuthorization.ownerUuid} + } + + + + + + ); \ No newline at end of file diff --git a/src/views-components/api-client-authorizations-dialog/remove-dialog.tsx b/src/views-components/api-client-authorizations-dialog/remove-dialog.tsx new file mode 100644 index 00000000..47b6adc7 --- /dev/null +++ b/src/views-components/api-client-authorizations-dialog/remove-dialog.tsx @@ -0,0 +1,20 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 +import { Dispatch, compose } from 'redux'; +import { connect } from "react-redux"; +import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog"; +import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog"; +import { API_CLIENT_AUTHORIZATION_REMOVE_DIALOG, removeApiClientAuthorization } from '~/store/api-client-authorizations/api-client-authorizations-actions'; + +const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps) => ({ + onConfirm: () => { + props.closeDialog(); + dispatch(removeApiClientAuthorization(props.data.uuid)); + } +}); + +export const RemoveApiClientAuthorizationDialog = compose( + withDialog(API_CLIENT_AUTHORIZATION_REMOVE_DIALOG), + connect(null, mapDispatchToProps) +)(ConfirmationDialog); \ No newline at end of file diff --git a/src/views-components/context-menu/action-sets/api-client-authorization-action-set.ts b/src/views-components/context-menu/action-sets/api-client-authorization-action-set.ts new file mode 100644 index 00000000..b6f089a6 --- /dev/null +++ b/src/views-components/context-menu/action-sets/api-client-authorization-action-set.ts @@ -0,0 +1,31 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { + openApiClientAuthorizationAttributesDialog, + openApiClientAuthorizationRemoveDialog +} from '~/store/api-client-authorizations/api-client-authorizations-actions'; +import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab'; +import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set"; +import { AdvancedIcon, RemoveIcon, AttributesIcon } from "~/components/icon/icon"; + +export const apiClientAuthorizationActionSet: ContextMenuActionSet = [[{ + name: "Attributes", + icon: AttributesIcon, + execute: (dispatch, { uuid }) => { + dispatch(openApiClientAuthorizationAttributesDialog(uuid)); + } +}, { + name: "Advanced", + icon: AdvancedIcon, + execute: (dispatch, { uuid }) => { + dispatch(openAdvancedTabDialog(uuid)); + } +}, { + name: "Remove", + icon: RemoveIcon, + execute: (dispatch, { uuid }) => { + dispatch(openApiClientAuthorizationRemoveDialog(uuid)); + } +}]]; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index 3fa1ab30..35298d0e 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -55,6 +55,7 @@ const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet }; export enum ContextMenuKind { + API_CLIENT_AUTHORIZATION = "ApiClientAuthorization", ROOT_PROJECT = "RootProject", PROJECT = "Project", RESOURCE = "Resource", diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx index f4232a12..f385b9f6 100644 --- a/src/views-components/main-app-bar/account-menu.tsx +++ b/src/views-components/main-app-bar/account-menu.tsx @@ -12,7 +12,10 @@ import { logout } from '~/store/auth/auth-action'; import { RootState } from "~/store/store"; import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-token-dialog-actions'; import { openRepositoriesPanel } from "~/store/repositories/repositories-actions"; -import { navigateToSshKeys, navigateToKeepServices, navigateToComputeNodes } from '~/store/navigation/navigation-action'; +import { + navigateToSshKeys, navigateToKeepServices, navigateToComputeNodes, + navigateToApiClientAuthorizations +} from '~/store/navigation/navigation-action'; import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions"; interface AccountMenuProps { @@ -37,6 +40,7 @@ export const AccountMenu = connect(mapStateToProps)( dispatch(openRepositoriesPanel())}>Repositories dispatch(openCurrentTokenDialog)}>Current token dispatch(navigateToSshKeys)}>Ssh Keys + { user.isAdmin && dispatch(navigateToApiClientAuthorizations)}>Api Tokens } { user.isAdmin && dispatch(navigateToKeepServices)}>Keep Services } { user.isAdmin && dispatch(navigateToComputeNodes)}>Compute Nodes } My account diff --git a/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx b/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx new file mode 100644 index 00000000..bd50a593 --- /dev/null +++ b/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx @@ -0,0 +1,89 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// 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 +} from '@material-ui/core'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { MoreOptionsIcon } from '~/components/icon/icon'; +import { ApiClientAuthorization } from '~/models/api-client-authorization'; + +type CssRules = 'root' | 'tableRow'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + width: '100%', + overflow: 'auto' + }, + tableRow: { + '& td, th': { + whiteSpace: 'nowrap' + } + } +}); + +export interface ApiClientAuthorizationPanelRootActionProps { + openRowOptions: (event: React.MouseEvent, keepService: ApiClientAuthorization) => void; +} + +export interface ApiClientAuthorizationPanelRootDataProps { + apiClientAuthorizations: ApiClientAuthorization[]; + hasApiClientAuthorizations: boolean; +} + +type ApiClientAuthorizationPanelRootProps = ApiClientAuthorizationPanelRootActionProps + & ApiClientAuthorizationPanelRootDataProps & WithStyles; + +export const ApiClientAuthorizationPanelRoot = withStyles(styles)( + ({ classes, hasApiClientAuthorizations, apiClientAuthorizations, openRowOptions }: ApiClientAuthorizationPanelRootProps) => + + + {hasApiClientAuthorizations && + + + + + UUID + API Client ID + API Token + Created by IP address + Default owner + Expires at + Last used at + Last used by IP address + Scopes + User ID + + + + + {apiClientAuthorizations.map((apiClientAuthorizatio, index) => + + {apiClientAuthorizatio.uuid} + {apiClientAuthorizatio.apiClientId} + {apiClientAuthorizatio.apiToken} + {apiClientAuthorizatio.createdByIpAddress || '(none)'} + {apiClientAuthorizatio.defaultOwnerUuid || '(none)'} + {apiClientAuthorizatio.expiresAt || '(none)'} + {apiClientAuthorizatio.lastUsedAt || '(none)'} + {apiClientAuthorizatio.lastUsedByIpAddress || '(none)'} + {JSON.stringify(apiClientAuthorizatio.scopes)} + {apiClientAuthorizatio.userId} + + + openRowOptions(event, apiClientAuthorizatio)}> + + + + + )} + +
+
+
} +
+
+); \ No newline at end of file diff --git a/src/views/api-client-authorization-panel/api-client-authorization-panel.tsx b/src/views/api-client-authorization-panel/api-client-authorization-panel.tsx new file mode 100644 index 00000000..06d32bfe --- /dev/null +++ b/src/views/api-client-authorization-panel/api-client-authorization-panel.tsx @@ -0,0 +1,28 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { RootState } from '~/store/store'; +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { + ApiClientAuthorizationPanelRoot, + ApiClientAuthorizationPanelRootDataProps, + ApiClientAuthorizationPanelRootActionProps +} from '~/views/api-client-authorization-panel/api-client-authorization-panel-root'; +import { openApiClientAuthorizationContextMenu } from '~/store/context-menu/context-menu-actions'; + +const mapStateToProps = (state: RootState): ApiClientAuthorizationPanelRootDataProps => { + return { + apiClientAuthorizations: state.apiClientAuthorizations, + hasApiClientAuthorizations: state.apiClientAuthorizations.length > 0 + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch): ApiClientAuthorizationPanelRootActionProps => ({ + openRowOptions: (event, apiClientAuthorization) => { + dispatch(openApiClientAuthorizationContextMenu(event, apiClientAuthorization)); + } +}); + +export const ApiClientAuthorizationPanel = connect(mapStateToProps, mapDispatchToProps)(ApiClientAuthorizationPanelRoot); \ No newline at end of file diff --git a/src/views/compute-node-panel/compute-node-panel-root.tsx b/src/views/compute-node-panel/compute-node-panel-root.tsx index be3627b8..7b1013b3 100644 --- a/src/views/compute-node-panel/compute-node-panel-root.tsx +++ b/src/views/compute-node-panel/compute-node-panel-root.tsx @@ -60,7 +60,7 @@ export const ComputeNodePanelRoot = withStyles(styles)( {computeNodes.map((computeNode, index) => - {computeNode.uuid} + {JSON.stringify(computeNode.info, null, 4)} {computeNode.uuid} {computeNode.domain} {formatDate(computeNode.firstPingAt) || '(none)'} diff --git a/src/views/virtual-machine-panel/virtual-machine-panel.tsx b/src/views/virtual-machine-panel/virtual-machine-panel.tsx index 5dbd3f09..d5d34555 100644 --- a/src/views/virtual-machine-panel/virtual-machine-panel.tsx +++ b/src/views/virtual-machine-panel/virtual-machine-panel.tsx @@ -12,7 +12,7 @@ import { Link } from 'react-router-dom'; import { compose, Dispatch } from 'redux'; import { saveRequestedDate, loadVirtualMachinesData } from '~/store/virtual-machines/virtual-machines-actions'; import { RootState } from '~/store/store'; -import { ListResults } from '~/services/common-service/common-resource-service'; +import { ListResults } from '~/services/common-service/common-service'; import { HelpIcon, MoreOptionsIcon } from '~/components/icon/icon'; import { VirtualMachineLogins, VirtualMachinesResource } from '~/models/virtual-machines'; import { Routes } from '~/routes/routes'; diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 92c2438b..1f2c6020 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -53,16 +53,19 @@ import { ProjectPropertiesDialog } from '~/views-components/project-properties-d import { RepositoriesPanel } from '~/views/repositories-panel/repositories-panel'; import { KeepServicePanel } from '~/views/keep-service-panel/keep-service-panel'; import { ComputeNodePanel } from '~/views/compute-node-panel/compute-node-panel'; +import { ApiClientAuthorizationPanel } from '~/views/api-client-authorization-panel/api-client-authorization-panel'; import { RepositoriesSampleGitDialog } from '~/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog'; import { RepositoryAttributesDialog } from '~/views-components/repository-attributes-dialog/repository-attributes-dialog'; import { CreateRepositoryDialog } from '~/views-components/dialog-forms/create-repository-dialog'; import { RemoveRepositoryDialog } from '~/views-components/repository-remove-dialog/repository-remove-dialog'; import { CreateSshKeyDialog } from '~/views-components/dialog-forms/create-ssh-key-dialog'; import { PublicKeyDialog } from '~/views-components/ssh-keys-dialog/public-key-dialog'; +import { RemoveApiClientAuthorizationDialog } from '~/views-components/api-client-authorizations-dialog/remove-dialog'; import { RemoveComputeNodeDialog } from '~/views-components/compute-nodes-dialog/remove-dialog'; import { RemoveKeepServiceDialog } from '~/views-components/keep-services-dialog/remove-dialog'; import { RemoveSshKeyDialog } from '~/views-components/ssh-keys-dialog/remove-dialog'; import { RemoveVirtualMachineDialog } from '~/views-components/virtual-machines-dialog/remove-dialog'; +import { AttributesApiClientAuthorizationDialog } from '~/views-components/api-client-authorizations-dialog/attributes-dialog'; import { AttributesComputeNodeDialog } from '~/views-components/compute-nodes-dialog/attributes-dialog'; import { AttributesKeepServiceDialog } from '~/views-components/keep-services-dialog/attributes-dialog'; import { AttributesSshKeyDialog } from '~/views-components/ssh-keys-dialog/attributes-dialog'; @@ -141,6 +144,7 @@ export const WorkbenchPanel = + @@ -150,6 +154,7 @@ export const WorkbenchPanel = + @@ -173,6 +178,7 @@ export const WorkbenchPanel = + -- 2.30.2