From: Pawel Kowalczyk Date: Wed, 21 Nov 2018 10:37:04 +0000 (+0100) Subject: Merge branch 'master' into 13865-repositories X-Git-Tag: 1.3.0~19^2~2 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/c27efd822acfd3bd93fc3e2e3e24a24146811eb7?hp=856192df2e78cb7182122a77592044f5e51b1888 Merge branch 'master' into 13865-repositories refs #13865 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index 08f52d00..f863ba13 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -4,13 +4,13 @@ import * as React from 'react'; import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip } from '@material-ui/core'; -import MoreVertIcon from "@material-ui/icons/MoreVert"; import { ColumnSelector } from "../column-selector/column-selector"; import { DataTable, DataColumns } from "../data-table/data-table"; import { DataColumn, SortDirection } from "../data-table/data-column"; import { DataTableFilterItem } from '../data-table-filters/data-table-filters'; import { SearchInput } from '../search-input/search-input'; import { ArvadosTheme } from "~/common/custom-theme"; +import { MoreOptionsIcon } from '~/components/icon/icon'; type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton'; @@ -127,7 +127,7 @@ export const DataExplorer = withStyles(styles)( this.props.onContextMenu(event, item)}> - + diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index a0fbd6ef..b46195de 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -32,6 +32,7 @@ import Input from '@material-ui/icons/Input'; import InsertDriveFile from '@material-ui/icons/InsertDriveFile'; import LastPage from '@material-ui/icons/LastPage'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; +import ListAlt from '@material-ui/icons/ListAlt'; import Menu from '@material-ui/icons/Menu'; import MoreVert from '@material-ui/icons/MoreVert'; import Mail from '@material-ui/icons/Mail'; @@ -55,6 +56,7 @@ export type IconType = React.SFC<{ className?: string, style?: object }>; export const AddIcon: IconType = (props) => ; export const AddFavoriteIcon: IconType = (props) => ; export const AdvancedIcon: IconType = (props) => ; +export const AttributesIcon: IconType = (props) => ; export const BackIcon: IconType = (props) => ; export const CustomizeTableIcon: IconType = (props) => ; export const CommandIcon: IconType = (props) => ; diff --git a/src/index.tsx b/src/index.tsx index efe3a576..922720a4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -48,6 +48,7 @@ import { getBuildInfo } from '~/common/app-info'; import { DragDropContextProvider } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; import { initAdvanceFormProjectsTree } from '~/store/search-bar/search-bar-actions'; +import { repositoryActionSet } from '~/views-components/context-menu/action-sets/repository-action-set'; console.log(`Starting arvados [${getBuildInfo()}]`); @@ -64,6 +65,7 @@ addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet) addMenuActionSet(ContextMenuKind.PROCESS, processActionSet); addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet); addMenuActionSet(ContextMenuKind.TRASH, trashActionSet); +addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet); fetchConfig() .then(({ config, apiHost }) => { diff --git a/src/models/repositories.ts b/src/models/repositories.ts new file mode 100644 index 00000000..02b99beb --- /dev/null +++ b/src/models/repositories.ts @@ -0,0 +1,10 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Resource } from "~/models/resource"; + +export interface RepositoryResource extends Resource { + name: string; + cloneUrls: string[]; +} diff --git a/src/models/resource.ts b/src/models/resource.ts index b8156cf2..520520f7 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -28,6 +28,7 @@ export enum ResourceKind { LOG = "arvados#log", PROCESS = "arvados#containerRequest", PROJECT = "arvados#group", + REPOSITORY = "arvados#repository", USER = "arvados#user", WORKFLOW = "arvados#workflow", NONE = "arvados#none" @@ -39,6 +40,7 @@ export enum ResourceObjectType { CONTAINER_REQUEST = 'xvhdp', GROUP = 'j7d0g', LOG = '57u5n', + REPOSITORY = 's0uqq', USER = 'tpzed', WORKFLOW = '7fd4e', } @@ -73,6 +75,8 @@ export const extractUuidKind = (uuid: string = '') => { return ResourceKind.LOG; case ResourceObjectType.WORKFLOW: return ResourceKind.WORKFLOW; + case ResourceObjectType.REPOSITORY: + return ResourceKind.REPOSITORY; default: return undefined; } diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index ef9e9ebc..a4a53d70 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -4,8 +4,8 @@ import { History, Location } from 'history'; import { RootStore } from '~/store/store'; -import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute } from './routes'; -import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog } from '~/store/workbench/workbench-actions'; +import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute, matchRepositoriesRoute } from './routes'; +import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadRepositories } from '~/store/workbench/workbench-actions'; import { navigateToRootProject } from '~/store/navigation/navigation-action'; import { loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults } from '~//store/workbench/workbench-actions'; @@ -23,6 +23,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { const trashMatch = matchTrashRoute(pathname); const processMatch = matchProcessRoute(pathname); const processLogMatch = matchProcessLogRoute(pathname); + const repositoryMatch = matchRepositoriesRoute(pathname); const searchResultsMatch = matchSearchResultsRoute(pathname); const sharedWithMeMatch = matchSharedWithMeRoute(pathname); const runProcessMatch = matchRunProcessRoute(pathname); @@ -50,5 +51,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { store.dispatch(loadWorkflow); } else if (searchResultsMatch) { store.dispatch(loadSearchResults); + } else if(repositoryMatch) { + store.dispatch(loadRepositories); } }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index e5f34935..5dbecb45 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -16,6 +16,7 @@ export const Routes = { FAVORITES: '/favorites', TRASH: '/trash', PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`, + REPOSITORIES: '/repositories', SHARED_WITH_ME: '/shared-with-me', RUN_PROCESS: '/run-process', WORKFLOWS: '/workflows', @@ -70,9 +71,12 @@ export const matchSharedWithMeRoute = (route: string) => export const matchRunProcessRoute = (route: string) => matchPath(route, { path: Routes.RUN_PROCESS }); - + export const matchWorkflowRoute = (route: string) => matchPath(route, { path: Routes.WORKFLOWS }); export const matchSearchResultsRoute = (route: string) => matchPath(route, { path: Routes.SEARCH_RESULTS }); + +export const matchRepositoriesRoute = (route: string) => + matchPath(route, { path: Routes.REPOSITORIES }); \ 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 70c1df0e..6114c560 100644 --- a/src/services/common-service/common-resource-service.ts +++ b/src/services/common-service/common-resource-service.ts @@ -35,6 +35,7 @@ export enum CommonResourceServiceError { UNIQUE_VIOLATION = 'UniqueViolation', OWNERSHIP_CYCLE = 'OwnershipCycle', MODIFYING_CONTAINER_REQUEST_FINAL_STATE = 'ModifyingContainerRequestFinalState', + NAME_HAS_ALREADY_BEEN_TAKEN = 'NameHasAlreadyBeenTaken', UNKNOWN = 'Unknown', NONE = 'None' } @@ -150,6 +151,8 @@ export const getCommonResourceServiceError = (errorResponse: any) => { return CommonResourceServiceError.OWNERSHIP_CYCLE; case /Mounts cannot be modified in state 'Final'/.test(error): return CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE; + case /Name has already been taken/.test(error): + return CommonResourceServiceError.NAME_HAS_ALREADY_BEEN_TAKEN; default: return CommonResourceServiceError.UNKNOWN; } diff --git a/src/services/repositories-service/repositories-service.ts b/src/services/repositories-service/repositories-service.ts new file mode 100644 index 00000000..34f7f3f5 --- /dev/null +++ b/src/services/repositories-service/repositories-service.ts @@ -0,0 +1,22 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { AxiosInstance } from "axios"; +import { CommonResourceService } from "~/services/common-service/common-resource-service"; +import { RepositoryResource } from '~/models/repositories'; +import { ApiActions } from '~/services/api/api-actions'; + + export class RepositoriesService extends CommonResourceService { + constructor(serverApi: AxiosInstance, actions: ApiActions) { + super(serverApi, "repositories", actions); + } + + getAllPermissions() { + return CommonResourceService.defaultResponse( + this.serverApi + .get('repositories/get_all_permissions'), + this.actions + ); + } +} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index 5adf10b3..2bc955f2 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -24,6 +24,7 @@ import { ApiActions } from "~/services/api/api-actions"; import { WorkflowService } from "~/services/workflow-service/workflow-service"; import { SearchService } from '~/services/search-service/search-service'; import { PermissionService } from "~/services/permission-service/permission-service"; +import { RepositoriesService } from '~/services/repositories-service/repositories-service'; export type ServiceRepository = ReturnType; @@ -42,6 +43,7 @@ export const createServices = (config: Config, actions: ApiActions) => { const logService = new LogService(apiClient, actions); const permissionService = new PermissionService(apiClient, actions); const projectService = new ProjectService(apiClient, actions); + const repositoriesService = new RepositoriesService(apiClient, actions); const userService = new UserService(apiClient, actions); const workflowService = new WorkflowService(apiClient, actions); @@ -68,6 +70,7 @@ export const createServices = (config: Config, actions: ApiActions) => { logService, permissionService, projectService, + repositoriesService, searchService, tagService, userService, diff --git a/src/store/advanced-tab/advanced-tab.ts b/src/store/advanced-tab/advanced-tab.ts index ba0cf77d..6ad8af22 100644 --- a/src/store/advanced-tab/advanced-tab.ts +++ b/src/store/advanced-tab/advanced-tab.ts @@ -14,6 +14,7 @@ import { CollectionResource } from '~/models/collection'; import { ProjectResource } from '~/models/project'; import { ServiceRepository } from '~/services/services'; import { FilterBuilder } from '~/services/api/filter-builder'; +import { RepositoryResource } from '~/models/repositories'; export const ADVANCED_TAB_DIALOG = 'advancedTabDialog'; @@ -46,34 +47,46 @@ enum ProjectData { DELETE_AT = 'delete_at' } -export const openAdvancedTabDialog = (uuid: string) => +enum RepositoryData { + REPOSITORY = 'repository', + CREATED_AT = 'created_at' +} + +export const openAdvancedTabDialog = (uuid: string, index?: number) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const { resources } = getState(); const kind = extractUuidKind(uuid); const data = getResource(uuid)(resources); - const user = await services.userService.get(data.ownerUuid); - const metadata = await services.linkService.list({ - filters: new FilterBuilder() - .addEqual('headUuid', uuid) - .getFilters() - }); - if (data) { - if (kind === ResourceKind.COLLECTION) { - const dataCollection: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, collectionApiResponse, data, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, data.storageClassesConfirmed); - dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataCollection })); - } else if (kind === ResourceKind.PROCESS) { - const dataProcess: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, containerRequestApiResponse, data, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, data.outputName); - dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProcess })); - } else if (kind === ResourceKind.PROJECT) { - const dataProject: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, groupRequestApiResponse, data, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, data.deleteAt); - dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProject })); + const repositoryData = getState().repositories.items[index!]; + if (data || repositoryData) { + if (data) { + const user = await services.userService.get(data.ownerUuid); + const metadata = await services.linkService.list({ + filters: new FilterBuilder() + .addEqual('headUuid', uuid) + .getFilters() + }); + if (kind === ResourceKind.COLLECTION) { + const dataCollection: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, collectionApiResponse, data, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, data.storageClassesConfirmed); + dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataCollection })); + } else if (kind === ResourceKind.PROCESS) { + const dataProcess: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, containerRequestApiResponse, data, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, data.outputName); + dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProcess })); + } else if (kind === ResourceKind.PROJECT) { + const dataProject: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, groupRequestApiResponse, data, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, data.deleteAt); + dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProject })); + } + + } else if (kind === ResourceKind.REPOSITORY) { + const dataRepository: AdvancedTabDialogData = advancedTabData(uuid, '', '', repositoryApiResponse, repositoryData, RepositoryData.REPOSITORY, 'repositories', RepositoryData.CREATED_AT, repositoryData.createdAt); + dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataRepository })); } } else { dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR })); } }; -const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: CollectionData | ProcessData | ProjectData, resourcePrefix: GroupContentsResourcePrefix, resourceKindProperty: CollectionData | ProcessData | ProjectData, property: any) => { +const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: CollectionData | ProcessData | ProjectData | RepositoryData, resourcePrefix: GroupContentsResourcePrefix | 'repositories', resourceKindProperty: CollectionData | ProcessData | ProjectData | RepositoryData, property: any) => { return { uuid, user, @@ -82,9 +95,9 @@ const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind pythonHeader: pythonHeader(resourceKind), pythonExample: pythonExample(uuid, resourcePrefix), cliGetHeader: cliGetHeader(resourceKind), - cliGetExample: cliGetExample(uuid, resourcePrefix), + cliGetExample: cliGetExample(uuid, resourceKind), cliUpdateHeader: cliUpdateHeader(resourceKind, resourceKindProperty), - cliUpdateExample: cliUpdateExample(uuid, resourceKind, property, resourceKind), + cliUpdateExample: cliUpdateExample(uuid, resourceKind, property, resourceKindProperty), curlHeader: curlHeader(resourceKind, resourceKindProperty), curlExample: curlExample(uuid, resourcePrefix, property, resourceKind, resourceKindProperty), }; @@ -104,8 +117,8 @@ const pythonExample = (uuid: string, resourcePrefix: string) => { const cliGetHeader = (resourceKind: string) => `An example arv command to get a ${resourceKind} using its uuid:`; -const cliGetExample = (uuid: string, resourcePrefix: string) => { - const cliGetExample = `arv ${resourcePrefix} get \\ +const cliGetExample = (uuid: string, resourceKind: string) => { + const cliGetExample = `arv ${resourceKind} get \\ --uuid ${uuid}`; return cliGetExample; @@ -227,5 +240,18 @@ const groupRequestApiResponse = (apiResponse: ProjectResource) => { "delete_at": ${stringify(deleteAt)}, "properties": ${stringifyObject(properties)}`; + return response; +}; + +const repositoryApiResponse = (apiResponse: RepositoryResource) => { + const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name } = apiResponse; + const response = `"uuid": "${uuid}", +"owner_uuid": "${ownerUuid}", +"modified_by_client_uuid": ${stringify(modifiedByClientUuid)}, +"modified_by_user_uuid": ${stringify(modifiedByUserUuid)}, +"modified_at": ${stringify(modifiedAt)}, +"name": ${stringify(name)}, +"created_at": "${createdAt}"`; + return response; }; \ 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 32bc47b6..596ac87b 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -12,8 +12,8 @@ import { ProjectResource } from '~/models/project'; import { UserResource } from '~/models/user'; import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions'; import { extractUuidKind, ResourceKind } from '~/models/resource'; -import { matchProcessRoute } from '~/routes/routes'; import { Process } from '~/store/processes/process'; +import { RepositoryResource } from '~/models/repositories'; export const contextMenuActions = unionize({ OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(), @@ -30,6 +30,7 @@ export type ContextMenuResource = { kind: ResourceKind, menuKind: ContextMenuKind; isTrashed?: boolean; + index?: number }; export const isKeyboardClick = (event: React.MouseEvent) => event.nativeEvent.detail === 0; @@ -60,6 +61,18 @@ export const openCollectionFilesContextMenu = (event: React.MouseEvent, index: number, repository: RepositoryResource) => + (dispatch: Dispatch, getState: () => RootState) => { + dispatch(openContextMenu(event, { + name: '', + uuid: repository.uuid, + ownerUuid: repository.ownerUuid, + kind: ResourceKind.REPOSITORY, + menuKind: ContextMenuKind.REPOSITORY, + index + })); + }; + export const openRootProjectContextMenu = (event: React.MouseEvent, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => { const res = getResource(projectUuid)(getState().resources); diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index b63fc2cb..ce599699 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -61,3 +61,5 @@ export const navigateToSharedWithMe = push(Routes.SHARED_WITH_ME); export const navigateToRunProcess = push(Routes.RUN_PROCESS); export const navigateToSearchResults = push(Routes.SEARCH_RESULTS); + +export const navigateToRepositories = push(Routes.REPOSITORIES); diff --git a/src/store/repositories/repositories-actions.ts b/src/store/repositories/repositories-actions.ts new file mode 100644 index 00000000..a672738f --- /dev/null +++ b/src/store/repositories/repositories-actions.ts @@ -0,0 +1,107 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Dispatch } from "redux"; +import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action'; +import { RootState } from '~/store/store'; +import { ServiceRepository } from "~/services/services"; +import { navigateToRepositories } from "~/store/navigation/navigation-action"; +import { unionize, ofType, UnionOf } from "~/common/unionize"; +import { dialogActions } from '~/store/dialog/dialog-actions'; +import { RepositoryResource } from "~/models/repositories"; +import { startSubmit, reset, stopSubmit } from "redux-form"; +import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service"; +import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions'; + +export const repositoriesActions = unionize({ + SET_REPOSITORIES: ofType(), +}); + +export type RepositoriesActions = UnionOf; + +export const REPOSITORIES_PANEL = 'repositoriesPanel'; +export const REPOSITORIES_SAMPLE_GIT_DIALOG = 'repositoriesSampleGitDialog'; +export const REPOSITORY_ATTRIBUTES_DIALOG = 'repositoryAttributesDialog'; +export const REPOSITORY_CREATE_FORM_NAME = 'repositoryCreateFormName'; +export const REPOSITORY_REMOVE_DIALOG = 'repositoryRemoveDialog'; + +export const openRepositoriesSampleGitDialog = () => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const uuidPrefix = getState().properties.uuidPrefix; + dispatch(dialogActions.OPEN_DIALOG({ id: REPOSITORIES_SAMPLE_GIT_DIALOG, data: { uuidPrefix } })); + }; + +export const openRepositoryAttributes = (index: number) => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const repositoryData = getState().repositories.items[index]; + dispatch(dialogActions.OPEN_DIALOG({ id: REPOSITORY_ATTRIBUTES_DIALOG, data: { repositoryData } })); + }; + +export const openRepositoryCreateDialog = () => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const userUuid = await services.authService.getUuid(); + const user = await services.userService.get(userUuid!); + dispatch(reset(REPOSITORY_CREATE_FORM_NAME)); + dispatch(dialogActions.OPEN_DIALOG({ id: REPOSITORY_CREATE_FORM_NAME, data: { user } })); + }; + +export const createRepository = (repository: RepositoryResource) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const userUuid = await services.authService.getUuid(); + const user = await services.userService.get(userUuid!); + dispatch(startSubmit(REPOSITORY_CREATE_FORM_NAME)); + try { + const newRepository = await services.repositoriesService.create({ name: `${user.username}/${repository.name}` }); + dispatch(dialogActions.CLOSE_DIALOG({ id: REPOSITORY_CREATE_FORM_NAME })); + dispatch(reset(REPOSITORY_CREATE_FORM_NAME)); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Repository has been successfully created.", hideDuration: 2000, kind: SnackbarKind.SUCCESS })); + dispatch(loadRepositoriesData()); + return newRepository; + } catch (e) { + const error = getCommonResourceServiceError(e); + if (error === CommonResourceServiceError.NAME_HAS_ALREADY_BEEN_TAKEN) { + dispatch(stopSubmit(REPOSITORY_CREATE_FORM_NAME, { name: 'Repository with the same name already exists.' })); + } + return undefined; + } + }; + +export const openRemoveRepositoryDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(dialogActions.OPEN_DIALOG({ + id: REPOSITORY_REMOVE_DIALOG, + data: { + title: 'Remove repository', + text: 'Are you sure you want to remove this repository?', + confirmButtonLabel: 'Remove', + uuid + } + })); + }; + +export const removeRepository = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' })); + await services.repositoriesService.delete(uuid); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 })); + dispatch(loadRepositoriesData()); + }; + +const repositoriesBindedActions = bindDataExplorerActions(REPOSITORIES_PANEL); + +export const openRepositoriesPanel = () => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(navigateToRepositories); + }; + +export const loadRepositoriesData = () => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const repositories = await services.repositoriesService.list(); + dispatch(repositoriesActions.SET_REPOSITORIES(repositories.items)); + }; + +export const loadRepositoriesPanel = () => + (dispatch: Dispatch) => { + dispatch(repositoriesBindedActions.REQUEST_ITEMS()); + }; \ No newline at end of file diff --git a/src/store/repositories/repositories-reducer.ts b/src/store/repositories/repositories-reducer.ts new file mode 100644 index 00000000..3ef82896 --- /dev/null +++ b/src/store/repositories/repositories-reducer.ts @@ -0,0 +1,20 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { repositoriesActions, RepositoriesActions } from '~/store/repositories/repositories-actions'; +import { RepositoryResource } from '~/models/repositories'; + +interface Repositories { + items: RepositoryResource[]; +} + +const initialState: Repositories = { + items: [] +}; + +export const repositoriesReducer = (state = initialState, action: RepositoriesActions): Repositories => + repositoriesActions.match(action, { + SET_REPOSITORIES: items => ({ ...state, items }), + default: () => state + }); \ No newline at end of file diff --git a/src/store/store.ts b/src/store/store.ts index fa2a5be9..5e648c99 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -43,6 +43,7 @@ import { searchBarReducer } from './search-bar/search-bar-reducer'; import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions'; import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service'; import { resourcesDataReducer } from "~/store/resources-data/resources-data-reducer"; +import { repositoriesReducer } from '~/store/repositories/repositories-reducer'; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -111,5 +112,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({ progressIndicator: progressIndicatorReducer, runProcessPanel: runProcessPanelReducer, appInfo: appInfoReducer, - searchBar: searchBarReducer + searchBar: searchBarReducer, + repositories: repositoriesReducer }); diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index aaf8f266..c6440fd9 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -3,23 +3,23 @@ // SPDX-License-Identifier: AGPL-3.0 import { Dispatch } from 'redux'; -import { RootState } from "../store"; +import { RootState } from "~/store/store"; import { loadDetailsPanel } from '~/store/details-panel/details-panel-action'; -import { snackbarActions } from '../snackbar/snackbar-actions'; -import { loadFavoritePanel } from '../favorite-panel/favorite-panel-action'; +import { snackbarActions } from '~/store/snackbar/snackbar-actions'; +import { loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action'; import { openProjectPanel, projectPanelActions, setIsProjectPanelTrashed } from '~/store/project-panel/project-panel-action'; -import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects } from '../side-panel-tree/side-panel-tree-actions'; -import { loadResource, updateResources } from '../resources/resources-actions'; +import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects } from '~/store/side-panel-tree/side-panel-tree-actions'; +import { loadResource, updateResources } from '~/store/resources/resources-actions'; import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action'; import { projectPanelColumns } from '~/views/project-panel/project-panel'; import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel'; import { matchRootRoute } from '~/routes/routes'; -import { setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs, setTrashBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions'; -import { navigateToProject } from '../navigation/navigation-action'; +import { setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs, setTrashBreadcrumbs, setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions'; +import { navigateToProject } from '~/store/navigation/navigation-action'; import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog'; import { ServiceRepository } from '~/services/services'; -import { getResource } from '../resources/resources'; -import { getProjectPanelCurrentUuid } from '../project-panel/project-panel-action'; +import { getResource } from '~/store/resources/resources'; +import { getProjectPanelCurrentUuid } from '~/store/project-panel/project-panel-action'; import * as projectCreateActions from '~/store/projects/project-create-actions'; import * as projectMoveActions from '~/store/projects/project-move-actions'; import * as projectUpdateActions from '~/store/projects/project-update-actions'; @@ -27,21 +27,21 @@ import * as collectionCreateActions from '~/store/collections/collection-create- import * as collectionCopyActions from '~/store/collections/collection-copy-actions'; import * as collectionUpdateActions from '~/store/collections/collection-update-actions'; import * as collectionMoveActions from '~/store/collections/collection-move-actions'; -import * as processesActions from '../processes/processes-actions'; +import * as processesActions from '~/store/processes/processes-actions'; import * as processMoveActions from '~/store/processes/process-move-actions'; import * as processUpdateActions from '~/store/processes/process-update-actions'; import * as processCopyActions from '~/store/processes/process-copy-actions'; import { trashPanelColumns } from "~/views/trash-panel/trash-panel"; import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action"; -import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-actions'; +import { initProcessLogsPanel } from '~/store/process-logs-panel/process-logs-panel-actions'; import { loadProcessPanel } from '~/store/process-panel/process-panel-actions'; import { sharedWithMePanelActions } from '~/store/shared-with-me-panel/shared-with-me-panel-actions'; -import { loadSharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel-actions'; +import { loadSharedWithMePanel } from '~/store/shared-with-me-panel/shared-with-me-panel-actions'; import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog'; import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions'; import { workflowPanelColumns } from '~/views/workflow-panel/workflow-panel-view'; import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions'; -import { getProgressIndicator } from '../progress-indicator/progress-indicator-reducer'; +import { getProgressIndicator } from '~/store/progress-indicator/progress-indicator-reducer'; import { ResourceKind, extractUuidKind } from '~/models/resource'; import { FilterBuilder } from '~/services/api/filter-builder'; import { GroupContentsResource } from '~/services/groups-service/groups-service'; @@ -53,6 +53,7 @@ import { collectionPanelActions } from "~/store/collection-panel/collection-pane import { CollectionResource } from "~/models/collection"; import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/search-results-panel/search-results-panel-actions'; import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view'; +import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions'; export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen'; @@ -390,6 +391,12 @@ export const loadSearchResults = handleFirstTimeLoad( await dispatch(loadSearchResultsPanel()); }); +export const loadRepositories = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + await dispatch(loadRepositoriesPanel()); + dispatch(setBreadcrumbs([{ label: 'Repositories' }])); + }); + const finishLoadingProject = (project: GroupContentsResource | string) => async (dispatch: Dispatch) => { const uuid = typeof project === 'string' ? project : project.uuid; diff --git a/src/validators/validators.tsx b/src/validators/validators.tsx index edc47265..37c1bd37 100644 --- a/src/validators/validators.tsx +++ b/src/validators/validators.tsx @@ -19,4 +19,6 @@ export const COPY_FILE_VALIDATION = [require]; export const MOVE_TO_VALIDATION = [require]; -export const PROCESS_NAME_VALIDATION = [require, maxLength(255)]; \ No newline at end of file +export const PROCESS_NAME_VALIDATION = [require, maxLength(255)]; + +export const REPOSITORY_NAME_VALIDATION = [require, maxLength(255)]; \ No newline at end of file diff --git a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx index 9a31a69e..8bce416d 100644 --- a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx +++ b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx @@ -78,7 +78,7 @@ export const AdvancedTabDialog = compose( {value === 0 &&
{dialogContentExample(apiResponse, classes)}
} - {value === 1 &&
{metadata.items.length > 0 ? : dialogContentHeader('(No metadata links found)')}
} + {value === 1 &&
{metadata !== '' && metadata.items.length > 0 ? : dialogContentHeader('(No metadata links found)')}
} {value === 2 && dialogContent(pythonHeader, pythonExample, classes)} {value === 3 &&
{dialogContent(cliGetHeader, cliGetExample, classes)} diff --git a/src/views-components/context-menu/action-sets/repository-action-set.ts b/src/views-components/context-menu/action-sets/repository-action-set.ts new file mode 100644 index 00000000..cf7fb883 --- /dev/null +++ b/src/views-components/context-menu/action-sets/repository-action-set.ts @@ -0,0 +1,35 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set"; +import { AdvancedIcon, RemoveIcon, ShareIcon, AttributesIcon } from "~/components/icon/icon"; +import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab"; +import { openRepositoryAttributes, openRemoveRepositoryDialog } from "~/store/repositories/repositories-actions"; +import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions"; + +export const repositoryActionSet: ContextMenuActionSet = [[{ + name: "Attributes", + icon: AttributesIcon, + execute: (dispatch, { index }) => { + dispatch(openRepositoryAttributes(index!)); + } +}, { + name: "Share", + icon: ShareIcon, + execute: (dispatch, { uuid }) => { + dispatch(openSharingDialog(uuid)); + } +}, { + name: "Advanced", + icon: AdvancedIcon, + execute: (dispatch, { uuid, index }) => { + dispatch(openAdvancedTabDialog(uuid, index)); + } +}, { + name: "Remove", + icon: RemoveIcon, + execute: (dispatch, { uuid }) => { + dispatch(openRemoveRepositoryDialog(uuid)); + } +}]]; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index b6d2b91b..30ecc981 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -68,5 +68,6 @@ export enum ContextMenuKind { TRASHED_COLLECTION = 'TrashedCollection', PROCESS = "Process", PROCESS_RESOURCE = 'ProcessResource', - PROCESS_LOGS = "ProcessLogs" + PROCESS_LOGS = "ProcessLogs", + REPOSITORY = "Repository" } diff --git a/src/views-components/dialog-create/dialog-repository-create.tsx b/src/views-components/dialog-create/dialog-repository-create.tsx new file mode 100644 index 00000000..45817224 --- /dev/null +++ b/src/views-components/dialog-create/dialog-repository-create.tsx @@ -0,0 +1,21 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { InjectedFormProps } from 'redux-form'; +import { WithDialogProps } from '~/store/dialog/with-dialog'; +import { FormDialog } from '~/components/form-dialog/form-dialog'; +import { RepositoryNameField } from '~/views-components/form-fields/repository-form-fields'; + +type DialogRepositoryProps = WithDialogProps<{}> & InjectedFormProps; + +export const DialogRepositoryCreate = (props: DialogRepositoryProps) => + ; + + diff --git a/src/views-components/dialog-forms/create-repository-dialog.ts b/src/views-components/dialog-forms/create-repository-dialog.ts new file mode 100644 index 00000000..04a13c0f --- /dev/null +++ b/src/views-components/dialog-forms/create-repository-dialog.ts @@ -0,0 +1,19 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { compose } from "redux"; +import { reduxForm } from 'redux-form'; +import { withDialog } from "~/store/dialog/with-dialog"; +import { createRepository, REPOSITORY_CREATE_FORM_NAME } from "~/store/repositories/repositories-actions"; +import { DialogRepositoryCreate } from "~/views-components/dialog-create/dialog-repository-create"; + +export const CreateRepositoryDialog = compose( + withDialog(REPOSITORY_CREATE_FORM_NAME), + reduxForm({ + form: REPOSITORY_CREATE_FORM_NAME, + onSubmit: (repositoryName, dispatch) => { + dispatch(createRepository(repositoryName)); + } + }) +)(DialogRepositoryCreate); \ No newline at end of file diff --git a/src/views-components/form-fields/repository-form-fields.tsx b/src/views-components/form-fields/repository-form-fields.tsx new file mode 100644 index 00000000..932a5fe2 --- /dev/null +++ b/src/views-components/form-fields/repository-form-fields.tsx @@ -0,0 +1,30 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { Field } from "redux-form"; +import { TextField } from "~/components/text-field/text-field"; +import { REPOSITORY_NAME_VALIDATION } from "~/validators/validators"; +import { Grid } from "@material-ui/core"; + +export const RepositoryNameField = (props: any) => + + + {props.data.user.username}/ + + + + + + .git + + + It may take a minute or two before you can clone your new repository. + + ; \ No newline at end of file diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx index fdd8123f..c643fef2 100644 --- a/src/views-components/main-app-bar/account-menu.tsx +++ b/src/views-components/main-app-bar/account-menu.tsx @@ -10,7 +10,8 @@ import { UserPanelIcon } from "~/components/icon/icon"; import { DispatchProp, connect } from 'react-redux'; import { logout } from "~/store/auth/auth-action"; import { RootState } from "~/store/store"; -import { openCurrentTokenDialog } from '../../store/current-token-dialog/current-token-dialog-actions'; +import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-token-dialog-actions'; +import { openRepositoriesPanel } from "~/store/repositories/repositories-actions"; interface AccountMenuProps { user?: User; @@ -30,6 +31,7 @@ export const AccountMenu = connect(mapStateToProps)( {getUserFullname(user)} + dispatch(openRepositoriesPanel())}>Repositories dispatch(openCurrentTokenDialog)}>Current token My account dispatch(logout())}>Logout diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx index b0478377..2039f6b8 100644 --- a/src/views-components/main-content-bar/main-content-bar.tsx +++ b/src/views-components/main-content-bar/main-content-bar.tsx @@ -6,10 +6,9 @@ import * as React from "react"; import { Toolbar, IconButton, Tooltip, Grid } from "@material-ui/core"; import { DetailsIcon } from "~/components/icon/icon"; import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs"; -import { detailsPanelActions } from "~/store/details-panel/details-panel-action"; import { connect } from 'react-redux'; import { RootState } from '~/store/store'; -import { matchWorkflowRoute } from '~/routes/routes'; +import { matchWorkflowRoute, matchRepositoriesRoute } from '~/routes/routes'; import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action'; interface MainContentBarProps { @@ -23,8 +22,14 @@ const isWorkflowPath = ({ router }: RootState) => { return !!match; }; +const isRepositoriesPath = ({ router }: RootState) => { + const pathname = router.location ? router.location.pathname : ''; + const match = matchRepositoriesRoute(pathname); + return !!match; +}; + export const MainContentBar = connect((state: RootState) => ({ - buttonVisible: !isWorkflowPath(state) + buttonVisible: !isWorkflowPath(state) && !isRepositoriesPath(state) }), { onDetailsPanelToggle: toggleDetailsPanel })((props: MainContentBarProps) => @@ -34,11 +39,11 @@ export const MainContentBar = connect((state: RootState) => ({ - {props.buttonVisible ? + {props.buttonVisible && - : null} + } ); diff --git a/src/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog.tsx b/src/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog.tsx new file mode 100644 index 00000000..1a00e977 --- /dev/null +++ b/src/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog.tsx @@ -0,0 +1,78 @@ +// 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 { REPOSITORIES_SAMPLE_GIT_DIALOG } from "~/store/repositories/repositories-actions"; +import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet'; +import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { compose } from "redux"; + +type CssRules = 'codeSnippet' | 'link' | 'spacing'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + codeSnippet: { + borderRadius: theme.spacing.unit * 0.5, + border: '1px solid', + borderColor: theme.palette.grey["400"], + }, + link: { + textDecoration: 'none', + color: theme.palette.primary.main, + "&:hover": { + color: theme.palette.primary.dark, + transition: 'all 0.5s ease' + } + }, + spacing: { + paddingTop: theme.spacing.unit * 2 + } +}); + +interface RepositoriesSampleGitDataProps { + uuidPrefix: string; +} + +type RepositoriesSampleGitProps = RepositoriesSampleGitDataProps & WithStyles; + +export const RepositoriesSampleGitDialog = compose( + withDialog(REPOSITORIES_SAMPLE_GIT_DIALOG), + withStyles(styles))( + (props: WithDialogProps & RepositoriesSampleGitProps) => + + Sample git quick start: + + + + See also: + + + + + + + + + ); + +const snippetText = (uuidPrefix: string) => `git clone git@git.${uuidPrefix}.arvadosapi.com:arvados.git +cd arvados +# edit files +git add the/files/you/changed +git commit +git push +`; diff --git a/src/views-components/repository-attributes-dialog/repository-attributes-dialog.tsx b/src/views-components/repository-attributes-dialog/repository-attributes-dialog.tsx new file mode 100644 index 00000000..94a0e6ca --- /dev/null +++ b/src/views-components/repository-attributes-dialog/repository-attributes-dialog.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 { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Grid } from "@material-ui/core"; +import { WithDialogProps } from "~/store/dialog/with-dialog"; +import { withDialog } from '~/store/dialog/with-dialog'; +import { REPOSITORY_ATTRIBUTES_DIALOG } from "~/store/repositories/repositories-actions"; +import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { compose } from "redux"; +import { RepositoryResource } from "~/models/repositories"; + +type CssRules = 'rightContainer' | 'leftContainer' | 'spacing'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + rightContainer: { + textAlign: 'right', + paddingRight: theme.spacing.unit * 2, + color: theme.palette.grey["500"] + }, + leftContainer: { + textAlign: 'left', + paddingLeft: theme.spacing.unit * 2 + }, + spacing: { + paddingTop: theme.spacing.unit * 2 + }, +}); + +interface RepositoryAttributesDataProps { + repositoryData: RepositoryResource; +} + +type RepositoryAttributesProps = RepositoryAttributesDataProps & WithStyles; + +export const RepositoryAttributesDialog = compose( + withDialog(REPOSITORY_ATTRIBUTES_DIALOG), + withStyles(styles))( + (props: WithDialogProps & RepositoryAttributesProps) => + + Attributes + + + {props.data.repositoryData && attributes(props.data.repositoryData, props.classes)} + + + + + + + ); + +const attributes = (repositoryData: RepositoryResource, classes: any) => { + const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name } = repositoryData; + return ( + + + + Name + Owner uuid + Created at + Modified at + Modified by user uuid + Modified by client uuid + uuid + + + {name} + {ownerUuid} + {createdAt} + {modifiedAt} + {modifiedByUserUuid} + {modifiedByClientUuid} + {uuid} + + + + ); +}; diff --git a/src/views-components/repository-remove-dialog/repository-remove-dialog.ts b/src/views-components/repository-remove-dialog/repository-remove-dialog.ts new file mode 100644 index 00000000..148e78bd --- /dev/null +++ b/src/views-components/repository-remove-dialog/repository-remove-dialog.ts @@ -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 { removeRepository, REPOSITORY_REMOVE_DIALOG } from '~/store/repositories/repositories-actions'; + + const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps) => ({ + onConfirm: () => { + props.closeDialog(); + dispatch(removeRepository(props.data.uuid)); + } +}); + + export const RemoveRepositoryDialog = compose( + withDialog(REPOSITORY_REMOVE_DIALOG), + connect(null, mapDispatchToProps) +)(ConfirmationDialog); \ No newline at end of file diff --git a/src/views-components/rich-text-editor-dialog/rich-text-editor-dialog.tsx b/src/views-components/rich-text-editor-dialog/rich-text-editor-dialog.tsx index a9973554..86422baf 100644 --- a/src/views-components/rich-text-editor-dialog/rich-text-editor-dialog.tsx +++ b/src/views-components/rich-text-editor-dialog/rich-text-editor-dialog.tsx @@ -3,8 +3,8 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from "react"; -import { Dialog, DialogTitle, DialogContent, DialogActions, Button, DialogContentText } from "@material-ui/core"; -import { WithDialogProps } from "../../store/dialog/with-dialog"; +import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from "@material-ui/core"; +import { WithDialogProps } from "~/store/dialog/with-dialog"; import { withDialog } from '~/store/dialog/with-dialog'; import { RICH_TEXT_EDITOR_DIALOG_NAME } from "~/store/rich-text-editor-dialog/rich-text-editor-dialog-actions"; import RichTextEditor from 'react-rte'; diff --git a/src/views/repositories-panel/repositories-panel.tsx b/src/views/repositories-panel/repositories-panel.tsx new file mode 100644 index 00000000..262f3cc3 --- /dev/null +++ b/src/views/repositories-panel/repositories-panel.tsx @@ -0,0 +1,153 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip, IconButton } from '@material-ui/core'; +import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { Link } from 'react-router-dom'; +import { Dispatch, compose } from 'redux'; +import { RootState } from '~/store/store'; +import { HelpIcon, AddIcon, MoreOptionsIcon } from '~/components/icon/icon'; +import { loadRepositoriesData, openRepositoriesSampleGitDialog, openRepositoryCreateDialog } from '~/store/repositories/repositories-actions'; +import { RepositoryResource } from '~/models/repositories'; +import { openRepositoryContextMenu } from '~/store/context-menu/context-menu-actions'; + + +type CssRules = 'link' | 'button' | 'icon' | 'iconRow' | 'moreOptionsButton' | 'moreOptions' | 'cloneUrls'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + link: { + textDecoration: 'none', + color: theme.palette.primary.main, + "&:hover": { + color: theme.palette.primary.dark, + transition: 'all 0.5s ease' + } + }, + button: { + textAlign: 'right', + alignSelf: 'center' + }, + icon: { + cursor: 'pointer', + color: theme.palette.grey["500"], + "&:hover": { + color: theme.palette.common.black, + transition: 'all 0.5s ease' + } + }, + iconRow: { + paddingTop: theme.spacing.unit * 2, + textAlign: 'right' + }, + moreOptionsButton: { + padding: 0 + }, + moreOptions: { + textAlign: 'right', + '&:last-child': { + paddingRight: 0 + } + }, + cloneUrls: { + whiteSpace: 'pre-wrap' + } +}); + +const mapStateToProps = (state: RootState) => { + return { + repositories: state.repositories.items + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch): Pick => ({ + loadRepositories: () => dispatch(loadRepositoriesData()), + onOptionsMenuOpen: (event, index, repository) => { + dispatch(openRepositoryContextMenu(event, index, repository)); + }, + openRepositoriesSampleGitDialog: () => dispatch(openRepositoriesSampleGitDialog()), + openRepositoryCreateDialog: () => dispatch(openRepositoryCreateDialog()) +}); + +interface RepositoriesActionProps { + loadRepositories: () => void; + onOptionsMenuOpen: (event: React.MouseEvent, index: number, repository: RepositoryResource) => void; + openRepositoriesSampleGitDialog: () => void; + openRepositoryCreateDialog: () => void; +} + +interface RepositoriesDataProps { + repositories: RepositoryResource[]; +} + + +type RepositoriesProps = RepositoriesDataProps & RepositoriesActionProps & WithStyles; + +export const RepositoriesPanel = compose( + withStyles(styles), + connect(mapStateToProps, mapDispatchToProps))( + class extends React.Component { + componentDidMount() { + this.props.loadRepositories(); + } + render() { + const { classes, repositories, onOptionsMenuOpen, openRepositoriesSampleGitDialog, openRepositoryCreateDialog } = this.props; + return ( + + + + + + When you are using an Arvados virtual machine, you should clone the https:// URLs. This will authenticate automatically using your API token.
+ In order to clone git repositories using SSH, add an SSH key to your account and clone the git@ URLs. +
+
+ + + +
+ +
+ + + + + +
+
+ + {repositories && + + + Name + URL + + + + + {repositories.map((repository, index) => + + {repository.name} + {repository.cloneUrls.join("\n")} + + + onOptionsMenuOpen(event, index, repository)} className={classes.moreOptionsButton}> + + + + + )} + +
} +
+
+
+ ); + } + } + ); \ No newline at end of file diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 8d1fb670..eee9911c 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -48,6 +48,11 @@ import { SharingDialog } from '~/views-components/sharing-dialog/sharing-dialog' import { AdvancedTabDialog } from '~/views-components/advanced-tab-dialog/advanced-tab-dialog'; import { ProcessInputDialog } from '~/views-components/process-input-dialog/process-input-dialog'; import { ProjectPropertiesDialog } from '~/views-components/project-properties-dialog/project-properties-dialog'; +import { RepositoriesPanel } from '~/views/repositories-panel/repositories-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'; type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content'; @@ -117,6 +122,7 @@ export const WorkbenchPanel = + @@ -132,6 +138,7 @@ export const WorkbenchPanel = + @@ -144,7 +151,10 @@ export const WorkbenchPanel = + + +