Merge branch 'master' into 13865-repositories
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 21 Nov 2018 10:37:04 +0000 (11:37 +0100)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 21 Nov 2018 10:37:04 +0000 (11:37 +0100)
refs #13865

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

32 files changed:
src/components/data-explorer/data-explorer.tsx
src/components/icon/icon.tsx
src/index.tsx
src/models/repositories.ts [new file with mode: 0644]
src/models/resource.ts
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/services/common-service/common-resource-service.ts
src/services/repositories-service/repositories-service.ts [new file with mode: 0644]
src/services/services.ts
src/store/advanced-tab/advanced-tab.ts
src/store/context-menu/context-menu-actions.ts
src/store/navigation/navigation-action.ts
src/store/repositories/repositories-actions.ts [new file with mode: 0644]
src/store/repositories/repositories-reducer.ts [new file with mode: 0644]
src/store/store.ts
src/store/workbench/workbench-actions.ts
src/validators/validators.tsx
src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
src/views-components/context-menu/action-sets/repository-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/context-menu.tsx
src/views-components/dialog-create/dialog-repository-create.tsx [new file with mode: 0644]
src/views-components/dialog-forms/create-repository-dialog.ts [new file with mode: 0644]
src/views-components/form-fields/repository-form-fields.tsx [new file with mode: 0644]
src/views-components/main-app-bar/account-menu.tsx
src/views-components/main-content-bar/main-content-bar.tsx
src/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog.tsx [new file with mode: 0644]
src/views-components/repository-attributes-dialog/repository-attributes-dialog.tsx [new file with mode: 0644]
src/views-components/repository-remove-dialog/repository-remove-dialog.ts [new file with mode: 0644]
src/views-components/rich-text-editor-dialog/rich-text-editor-dialog.tsx
src/views/repositories-panel/repositories-panel.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx

index 08f52d00d37663f321106be226cea10c81ef08f0..f863ba13617adcc50c6ded0aac032445787079eb 100644 (file)
@@ -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)(
             <Grid container justify="center">
                 <Tooltip title="More options" disableFocusListener>
                     <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
-                        <MoreVertIcon />
+                        <MoreOptionsIcon />
                     </IconButton>
                 </Tooltip>
             </Grid>
index a0fbd6ef970b46eb016d6e44599207be2c7c5bd8..b46195de876e385d8ac721b38c88c122945b630c 100644 (file)
@@ -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) => <Add {...props} />;
 export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
 export const AdvancedIcon: IconType = (props) => <SettingsApplications {...props} />;
+export const AttributesIcon: IconType = (props) => <ListAlt {...props} />;
 export const BackIcon: IconType = (props) => <ArrowBack {...props} />;
 export const CustomizeTableIcon: IconType = (props) => <Menu {...props} />;
 export const CommandIcon: IconType = (props) => <LastPage {...props} />;
index efe3a576df3a7ca03bcade88819442f90c0a8663..922720a411c29bf9eccd9b17b766605f8dc89f31 100644 (file)
@@ -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 (file)
index 0000000..02b99be
--- /dev/null
@@ -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[];
+}
index b8156cf2d12c0c7990e5f8aa8e869626f8bf17bc..520520f76eb1d39c65c04378968b9c4de3093e5e 100644 (file)
@@ -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;
     }
index ef9e9ebcef15a01c271e7e6461b348f4e4b8237c..a4a53d7022300540721a961339888a7e786c87ee 100644 (file)
@@ -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);
     }
 };
index e5f3493539e3cbaae317e95fc1b38d3f89cb032f..5dbecb45245f566d83e1fbb63cb45c43edeca3df 100644 (file)
@@ -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<ResourceRouteParams>(route, { path: Routes.WORKFLOWS });
 
 export const matchSearchResultsRoute = (route: string) =>
     matchPath<ResourceRouteParams>(route, { path: Routes.SEARCH_RESULTS });
+
+export const matchRepositoriesRoute = (route: string) =>
+    matchPath<ResourceRouteParams>(route, { path: Routes.REPOSITORIES });
\ No newline at end of file
index 70c1df0e2bc5e665a3c62c05b39b7a50a6705c62..6114c560526e7b598c033b0a9c6ae4435bd10047 100644 (file)
@@ -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 (file)
index 0000000..34f7f3f
--- /dev/null
@@ -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<RepositoryResource> {
+    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
index 5adf10b387891b0fecd1efddcfbdade21badb4e5..2bc955f24c71ca8599f15127ae3a5b94d97662b7 100644 (file)
@@ -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<typeof createServices>;
 
@@ -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,
index ba0cf77db75e6fc2bcacc8183e5bff2d551e902e..6ad8af22a67ac8295d32793c276137fd2e6a4d98 100644 (file)
@@ -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<any>, getState: () => RootState, services: ServiceRepository) => {
         const { resources } = getState();
         const kind = extractUuidKind(uuid);
         const data = getResource<any>(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
index 32bc47b6b8cd04f6e884614abe1d7fb46bfbe0c5..596ac87b098f89503ce248a9c82786a5f5f6ae78 100644 (file)
@@ -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<HTMLElement>) =>
     event.nativeEvent.detail === 0;
@@ -60,6 +61,18 @@ export const openCollectionFilesContextMenu = (event: React.MouseEvent<HTMLEleme
         }));
     };
 
+export const openRepositoryContextMenu = (event: React.MouseEvent<HTMLElement>, index: number, repository: RepositoryResource) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+            dispatch<any>(openContextMenu(event, {
+                name: '',
+                uuid: repository.uuid,
+                ownerUuid: repository.ownerUuid,
+                kind: ResourceKind.REPOSITORY,
+                menuKind: ContextMenuKind.REPOSITORY,
+                index
+            }));
+    };
+
 export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const res = getResource<UserResource>(projectUuid)(getState().resources);
index b63fc2cb290af38edb74be1da9e9beb28c3a5a2c..ce5996997d02a0d22036f15466c9d69a69b57c75 100644 (file)
@@ -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 (file)
index 0000000..a672738
--- /dev/null
@@ -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<any>(),
+});
+
+export type RepositoriesActions = UnionOf<typeof repositoriesActions>;
+
+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<any>(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<any>(loadRepositoriesData());
+    };
+
+const repositoriesBindedActions = bindDataExplorerActions(REPOSITORIES_PANEL);
+
+export const openRepositoriesPanel = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch<any>(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 (file)
index 0000000..3ef8289
--- /dev/null
@@ -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
index fa2a5be9bb658dedcf8a87829b209da72474b2b8..5e648c99ef9efadd32dffba96a493cd8dde2c397 100644 (file)
@@ -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
 });
index aaf8f2665f9a1b07318c6a9d3179a0fbd3727d5d..c6440fd9593b66dc8cce94d9128d9e2587044eaf 100644 (file)
@@ -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<any>) => {
+        await dispatch(loadRepositoriesPanel());
+        dispatch(setBreadcrumbs([{ label: 'Repositories' }]));
+    });
+
 const finishLoadingProject = (project: GroupContentsResource | string) =>
     async (dispatch: Dispatch<any>) => {
         const uuid = typeof project === 'string' ? project : project.uuid;
index edc472657eb3178036fb0d8b3917dd0600e39ce5..37c1bd37458dc50cd293d4f9c9bf85ad2cb7311f 100644 (file)
@@ -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
index 9a31a69ed0c1786e4397b82b20f26c0940926a5b..8bce416d2ac8b9eb24ca3fd3246ae14f403b0347 100644 (file)
@@ -78,7 +78,7 @@ export const AdvancedTabDialog = compose(
                 </Tabs>
                 <DialogContent className={classes.content}>
                     {value === 0 && <div>{dialogContentExample(apiResponse, classes)}</div>}
-                    {value === 1 && <div>{metadata.items.length > 0 ? <MetadataTab items={metadata.items} uuid={uuid} user={user} /> : dialogContentHeader('(No metadata links found)')}</div>}
+                    {value === 1 && <div>{metadata !== '' && metadata.items.length > 0 ? <MetadataTab items={metadata.items} uuid={uuid} user={user} /> : dialogContentHeader('(No metadata links found)')}</div>}
                     {value === 2 && dialogContent(pythonHeader, pythonExample, classes)}
                     {value === 3 && <div>
                         {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 (file)
index 0000000..cf7fb88
--- /dev/null
@@ -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<any>(openRepositoryAttributes(index!));
+    }
+}, {
+    name: "Share",
+    icon: ShareIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openSharingDialog(uuid));
+    }
+}, {
+    name: "Advanced",
+    icon: AdvancedIcon,
+    execute: (dispatch, { uuid, index }) => {
+        dispatch<any>(openAdvancedTabDialog(uuid, index));
+    }
+}, {
+    name: "Remove",
+    icon: RemoveIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openRemoveRepositoryDialog(uuid));
+    }
+}]];
index b6d2b91b1a5e58dd07353ed19eb024e48425c629..30ecc9810eb40d338c2ec37d538fb17d549cebea 100644 (file)
@@ -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 (file)
index 0000000..4581722
--- /dev/null
@@ -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<any>;
+
+export const DialogRepositoryCreate = (props: DialogRepositoryProps) =>
+    <FormDialog
+        dialogTitle='Add new repository'
+        formFields={RepositoryNameField}
+        submitLabel='CREATE REPOSITORY'
+        {...props}
+    />;
+
+
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 (file)
index 0000000..04a13c0
--- /dev/null
@@ -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<any>({
+        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 (file)
index 0000000..932a5fe
--- /dev/null
@@ -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) =>
+    <Grid container style={{ marginTop: '0', paddingTop: '24px' }}>
+        <Grid item xs={3}>
+            {props.data.user.username}/
+        </Grid>
+        <Grid item xs={7} style={{ bottom: '24px', position: 'relative' }}>
+            <Field
+                name='name'
+                component={TextField}
+                validate={REPOSITORY_NAME_VALIDATION}
+                label="Name"
+                autoFocus={true} />
+        </Grid>
+        <Grid item xs={2}>
+            .git
+        </Grid>
+        <Grid item xs={12}>
+            It may take a minute or two before you can clone your new repository.
+        </Grid>
+    </Grid>;
\ No newline at end of file
index fdd8123f280273a5e88984a368436a464f0eefac..c643fef2583db913c8b2584ce58a9dacb923a8ff 100644 (file)
@@ -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)(
                 <MenuItem>
                     {getUserFullname(user)}
                 </MenuItem>
+                <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
                 <MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
                 <MenuItem>My account</MenuItem>
                 <MenuItem onClick={() => dispatch(logout())}>Logout</MenuItem>
index b0478377ba1a8f05490a662966a0cd7755d4bfc8..2039f6b8568680579b9ac352277fa022d2819bf2 100644 (file)
@@ -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) => ({
                     <Breadcrumbs />
                 </Grid>
                 <Grid item>
-                    {props.buttonVisible ? <Tooltip title="Additional Info">
+                    {props.buttonVisible && <Tooltip title="Additional Info">
                         <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
                             <DetailsIcon />
                         </IconButton>
-                    </Tooltip> : null}
+                    </Tooltip>}
                 </Grid>
             </Grid>
         </Toolbar>);
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 (file)
index 0000000..1a00e97
--- /dev/null
@@ -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<CssRules> = (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<CssRules>;
+
+export const RepositoriesSampleGitDialog = compose(
+    withDialog(REPOSITORIES_SAMPLE_GIT_DIALOG),
+    withStyles(styles))(
+        (props: WithDialogProps<RepositoriesSampleGitProps> & RepositoriesSampleGitProps) =>
+            <Dialog open={props.open}
+                onClose={props.closeDialog}
+                fullWidth
+                maxWidth='sm'>
+                <DialogTitle>Sample git quick start:</DialogTitle>
+                <DialogContent>
+                    <DefaultCodeSnippet
+                        className={props.classes.codeSnippet}
+                        lines={[snippetText(props.data.uuidPrefix)]} />
+                    <Typography variant="body2" className={props.classes.spacing}>
+                        See also:
+                        <div><a href="https://doc.arvados.org/user/getting_started/ssh-access-unix.html" className={props.classes.link} target="_blank">SSH access</a></div>
+                        <div><a href="https://doc.arvados.org/user/tutorials/tutorial-firstscript.html" className={props.classes.link} target="_blank">Writing a Crunch Script</a></div>
+                    </Typography>
+                </DialogContent>
+                <DialogActions>
+                    <Button
+                        variant='flat'
+                        color='primary'
+                        onClick={props.closeDialog}>
+                        Close
+                </Button>
+                </DialogActions>
+            </Dialog>
+    );
+
+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 (file)
index 0000000..94a0e6c
--- /dev/null
@@ -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<CssRules> = (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<CssRules>;
+
+export const RepositoryAttributesDialog = compose(
+    withDialog(REPOSITORY_ATTRIBUTES_DIALOG),
+    withStyles(styles))(
+        (props: WithDialogProps<RepositoryAttributesProps> & RepositoryAttributesProps) =>
+            <Dialog open={props.open}
+                onClose={props.closeDialog}
+                fullWidth
+                maxWidth="sm">
+                <DialogTitle>Attributes</DialogTitle>
+                <DialogContent>
+                    <Typography variant="body2" className={props.classes.spacing}>
+                        {props.data.repositoryData && attributes(props.data.repositoryData, props.classes)}
+                    </Typography>
+                </DialogContent>
+                <DialogActions>
+                    <Button
+                        variant='flat'
+                        color='primary'
+                        onClick={props.closeDialog}>
+                        Close
+                </Button>
+                </DialogActions>
+            </Dialog>
+    );
+
+const attributes = (repositoryData: RepositoryResource, classes: any) => {
+    const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name } = repositoryData;
+    return (
+        <span>
+            <Grid container direction="row">
+                <Grid item xs={5} className={classes.rightContainer}>
+                    <Grid item>Name</Grid>
+                    <Grid item>Owner uuid</Grid>
+                    <Grid item>Created at</Grid>
+                    <Grid item>Modified at</Grid>
+                    <Grid item>Modified by user uuid</Grid>
+                    <Grid item>Modified by client uuid</Grid>
+                    <Grid item>uuid</Grid>
+                </Grid>
+                <Grid item xs={7} className={classes.leftContainer}>
+                    <Grid item>{name}</Grid>
+                    <Grid item>{ownerUuid}</Grid>
+                    <Grid item>{createdAt}</Grid>
+                    <Grid item>{modifiedAt}</Grid>
+                    <Grid item>{modifiedByUserUuid}</Grid>
+                    <Grid item>{modifiedByClientUuid}</Grid>
+                    <Grid item>{uuid}</Grid>
+                </Grid>
+            </Grid>
+        </span>
+    );
+};
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 (file)
index 0000000..148e78b
--- /dev/null
@@ -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<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(removeRepository(props.data.uuid));
+    }
+});
+
+ export const RemoveRepositoryDialog = compose(
+    withDialog(REPOSITORY_REMOVE_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
\ No newline at end of file
index a997355410dc1f6eab0711142c23a6f27862e1c2..86422bafde731b5a36a288bb040934cf6a3fc84e 100644 (file)
@@ -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 (file)
index 0000000..262f3cc
--- /dev/null
@@ -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<CssRules> = (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<RepositoriesActionProps, 'onOptionsMenuOpen' | 'loadRepositories' | 'openRepositoriesSampleGitDialog' | 'openRepositoryCreateDialog'> => ({
+    loadRepositories: () => dispatch<any>(loadRepositoriesData()),
+    onOptionsMenuOpen: (event, index, repository) => {
+        dispatch<any>(openRepositoryContextMenu(event, index, repository));
+    },
+    openRepositoriesSampleGitDialog: () => dispatch<any>(openRepositoriesSampleGitDialog()),
+    openRepositoryCreateDialog: () => dispatch<any>(openRepositoryCreateDialog())
+});
+
+interface RepositoriesActionProps {
+    loadRepositories: () => void;
+    onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>, index: number, repository: RepositoryResource) => void;
+    openRepositoriesSampleGitDialog: () => void;
+    openRepositoryCreateDialog: () => void;
+}
+
+interface RepositoriesDataProps {
+    repositories: RepositoryResource[];
+}
+
+
+type RepositoriesProps = RepositoriesDataProps & RepositoriesActionProps & WithStyles<CssRules>;
+
+export const RepositoriesPanel = compose(
+    withStyles(styles),
+    connect(mapStateToProps, mapDispatchToProps))(
+        class extends React.Component<RepositoriesProps> {
+            componentDidMount() {
+                this.props.loadRepositories();
+            }
+            render() {
+                const { classes, repositories, onOptionsMenuOpen, openRepositoriesSampleGitDialog, openRepositoryCreateDialog } = this.props;
+                return (
+                    <Card>
+                        <CardContent>
+                            <Grid container direction="row">
+                                <Grid item xs={8}>
+                                    <Typography variant="body2">
+                                        When you are using an Arvados virtual machine, you should clone the https:// URLs. This will authenticate automatically using your API token. <br />
+                                        In order to clone git repositories using SSH, <Link to='' className={classes.link}>add an SSH key to your account</Link> and clone the git@ URLs.
+                                    </Typography>
+                                </Grid>
+                                <Grid item xs={4} className={classes.button}>
+                                    <Button variant="contained" color="primary" onClick={openRepositoryCreateDialog}>
+                                        <AddIcon /> NEW REPOSITORY
+                                    </Button>
+                                </Grid>
+                            </Grid>
+                            <Grid item xs={12}>
+                                <div className={classes.iconRow}>
+                                    <Tooltip title="Sample git quick start">
+                                        <IconButton className={classes.moreOptionsButton} onClick={openRepositoriesSampleGitDialog}>
+                                            <HelpIcon className={classes.icon} />
+                                        </IconButton>
+                                    </Tooltip>
+                                </div>
+                            </Grid>
+                            <Grid item xs={12}>
+                                {repositories && <Table>
+                                    <TableHead>
+                                        <TableRow>
+                                            <TableCell>Name</TableCell>
+                                            <TableCell>URL</TableCell>
+                                            <TableCell />
+                                        </TableRow>
+                                    </TableHead>
+                                    <TableBody>
+                                        {repositories.map((repository, index) =>
+                                            <TableRow key={index}>
+                                                <TableCell>{repository.name}</TableCell>
+                                                <TableCell className={classes.cloneUrls}>{repository.cloneUrls.join("\n")}</TableCell>
+                                                <TableCell className={classes.moreOptions}>
+                                                    <Tooltip title="More options" disableFocusListener>
+                                                        <IconButton onClick={event => onOptionsMenuOpen(event, index, repository)} className={classes.moreOptionsButton}>
+                                                            <MoreOptionsIcon />
+                                                        </IconButton>
+                                                    </Tooltip>
+                                                </TableCell>
+                                            </TableRow>)}
+                                    </TableBody>
+                                </Table>}
+                            </Grid>
+                        </CardContent>
+                    </Card>
+                );
+            }
+        }
+    );
\ No newline at end of file
index 8d1fb6700dc308c6709d2374299706f51e75adda..eee9911c163006d78a0ad200a4095ec252c328a9 100644 (file)
@@ -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 =
                                 <Route path={Routes.RUN_PROCESS} component={RunProcessPanel} />
                                 <Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
                                 <Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
+                                <Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
                             </Switch>
                         </Grid>
                     </Grid>
@@ -132,6 +138,7 @@ export const WorkbenchPanel =
             <CopyProcessDialog />
             <CreateCollectionDialog />
             <CreateProjectDialog />
+            <CreateRepositoryDialog />
             <CurrentTokenDialog />
             <FileRemoveDialog />
             <FilesUploadCollectionDialog />
@@ -144,7 +151,10 @@ export const WorkbenchPanel =
             <ProcessInputDialog />
             <ProjectPropertiesDialog />
             <RemoveProcessDialog />
+            <RemoveRepositoryDialog />
             <RenameFileDialog />
+            <RepositoryAttributesDialog />
+            <RepositoriesSampleGitDialog />
             <RichTextEditorDialog />
             <SharingDialog />
             <Snackbar />