this.request({
...config, url,
method: 'PUT',
- data,
+ data
})
+ upload = (url: string, path: string, files: File[], config: WebDAVRequestConfig = {}) => {
+ const fd = new FormData();
+ fd.append('path', path);
+ files.forEach((f, idx) => {
+ fd.append(`file-${idx}`, f);
+ });
+
+ return this.request({
+ ...config, url,
+ method: 'PUT',
+ data: fd
+ });
+ }
+
copy = (url: string, destination: string, config: WebDAVRequestConfig = {}) =>
this.request({
...config, url,
r.upload.addEventListener('progress', config.onUploadProgress);
}
- r.addEventListener('load', () => resolve(r));
- r.addEventListener('error', () => reject(r));
+ r.addEventListener('load', () => {
+ if (r.status === 404) {
+ return reject(r);
+ } else {
+ return resolve(r);
+ }
+ });
+
+ r.addEventListener('error', () => {
+ return reject(r);
+ });
+
+ r.upload.addEventListener('error', () => {
+ return reject(r);
+ });
r.send(config.data);
});
}
}
+
export interface WebDAVRequestConfig {
headers?: {
[key: string]: string;
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';
<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>
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';
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} />;
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()}]`);
addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
+addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet);
fetchConfig()
.then(({ config, apiHost }) => {
--- /dev/null
+// 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[];
+}
LOG = "arvados#log",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
+ REPOSITORY = "arvados#repository",
USER = "arvados#user",
VIRTUAL_MACHINE = "arvados#virtualMachine",
WORKFLOW = "arvados#workflow",
CONTAINER_REQUEST = 'xvhdp',
GROUP = 'j7d0g',
LOG = '57u5n',
+ REPOSITORY = 's0uqq',
USER = 'tpzed',
VIRTUAL_MACHINE = '2x53u',
WORKFLOW = '7fd4e',
return ResourceKind.WORKFLOW;
case ResourceObjectType.VIRTUAL_MACHINE:
return ResourceKind.VIRTUAL_MACHINE;
+ case ResourceObjectType.REPOSITORY:
+ return ResourceKind.REPOSITORY;
default:
return undefined;
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from '~/models/resource';
+
+export enum KeyType {
+ SSH = 'SSH'
+}
+
+export interface SshKeyResource extends Resource {
+ name: string;
+ keyType: KeyType;
+ authorizedUserUuid: string;
+ publicKey: string;
+ expiresAt: string;
+}
\ No newline at end of file
expect(Tree.getNode('Node 1')(newTree)).toEqual(initTreeNode({ id: 'Node 1', value: 'Value 1' }));
});
+ it('appends a subtree', () => {
+ const newTree = Tree.setNode(initTreeNode({ id: 'Node 1', value: 'Value 1' }))(tree);
+ const subtree = Tree.setNode(initTreeNode({ id: 'Node 2', value: 'Value 2' }))(Tree.createTree());
+ const mergedTree = Tree.appendSubtree('Node 1', subtree)(newTree);
+ expect(Tree.getNode('Node 1')(mergedTree)).toBeDefined();
+ expect(Tree.getNode('Node 2')(mergedTree)).toBeDefined();
+ });
+
it('adds new node reference to parent children', () => {
const newTree = pipe(
Tree.setNode(initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' })),
initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 'Value 2' }),
].reduce((tree, node) => Tree.setNode(node)(tree), tree);
const mappedTree = Tree.mapTreeValues<string, number>(value => parseInt(value.split(' ')[1], 10))(newTree);
- expect(Tree.getNode('Node 2')(mappedTree)).toEqual(initTreeNode({id: 'Node 2', parent: 'Node 1', value: 2 }));
+ expect(Tree.getNode('Node 2')(mappedTree)).toEqual(initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 2 }));
});
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { pipe } from 'lodash/fp';
+import { pipe, map, reduce } from 'lodash/fp';
export type Tree<T> = Record<string, TreeNode<T>>;
export const TREE_ROOT_ID = '';
export const getNode = (id: string) => <T>(tree: Tree<T>): TreeNode<T> | undefined => tree[id];
+export const appendSubtree = <T>(id: string, subtree: Tree<T>) => (tree: Tree<T>) =>
+ pipe(
+ getNodeDescendants(''),
+ map(node => node.parent === '' ? { ...node, parent: id } : node),
+ reduce((newTree, node) => setNode(node)(newTree), tree)
+ )(subtree) as Tree<T>;
+
export const setNode = <T>(node: TreeNode<T>) => (tree: Tree<T>): Tree<T> => {
return pipe(
(tree: Tree<T>) => getNode(node.id)(tree) === node
import { History, Location } from 'history';
import { RootStore } from '~/store/store';
-import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute, matchVirtualMachineRoute } from './routes';
-import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadVirtualMachines } from '~/store/workbench/workbench-actions';
+import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute } from './routes';
+import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadSshKeys, loadRepositories, loadVirtualMachines } from '~/store/workbench/workbench-actions';
import { navigateToRootProject } from '~/store/navigation/navigation-action';
import { loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults } from '~//store/workbench/workbench-actions';
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);
const virtualMachineMatch = matchVirtualMachineRoute(pathname);
const workflowMatch = matchWorkflowRoute(pathname);
+ const sshKeysMatch = matchSshKeysRoute(pathname);
if (projectMatch) {
store.dispatch(loadProject(projectMatch.params.id));
store.dispatch(loadSearchResults);
} else if (virtualMachineMatch) {
store.dispatch(loadVirtualMachines);
+ } else if(repositoryMatch) {
+ store.dispatch(loadRepositories);
+ } else if (sshKeysMatch) {
+ store.dispatch(loadSshKeys);
}
};
FAVORITES: '/favorites',
TRASH: '/trash',
PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`,
+ REPOSITORIES: '/repositories',
SHARED_WITH_ME: '/shared-with-me',
RUN_PROCESS: '/run-process',
VIRTUAL_MACHINES: '/virtual-machines',
WORKFLOWS: '/workflows',
- SEARCH_RESULTS: '/search-results'
+ SEARCH_RESULTS: '/search-results',
+ SSH_KEYS: `/ssh-keys`
};
export const getResourceUrl = (uuid: string) => {
export const matchVirtualMachineRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.VIRTUAL_MACHINES });
+
+export const matchRepositoriesRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.REPOSITORIES });
+
+export const matchSshKeysRoute = (route: string) =>
+ matchPath(route, { path: Routes.SSH_KEYS });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { SshKeyResource } from '~/models/ssh-key';
+import { CommonResourceService, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
+import { ApiActions } from "~/services/api/api-actions";
+
+export enum AuthorizedKeysServiceError {
+ UNIQUE_PUBLIC_KEY = 'UniquePublicKey',
+ INVALID_PUBLIC_KEY = 'InvalidPublicKey',
+}
+
+export class AuthorizedKeysService extends CommonResourceService<SshKeyResource> {
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "authorized_keys", actions);
+ }
+}
+
+export const getAuthorizedKeysServiceError = (errorResponse: any) => {
+ if ('errors' in errorResponse && 'errorToken' in errorResponse) {
+ const error = errorResponse.errors.join('');
+ switch (true) {
+ case /Public key does not appear to be a valid ssh-rsa or dsa public key/.test(error):
+ return AuthorizedKeysServiceError.INVALID_PUBLIC_KEY;
+ case /Public key already exists in the database, use a different key./.test(error):
+ return AuthorizedKeysServiceError.UNIQUE_PUBLIC_KEY;
+ default:
+ return CommonResourceServiceError.UNKNOWN;
+ }
+ }
+ return CommonResourceServiceError.NONE;
+};
\ No newline at end of file
private async uploadFile(collectionUuid: string, file: File, fileId: number, onProgress: UploadProgress = () => { return; }) {
const fileURL = `c=${collectionUuid}/${file.name}`;
- const fileContent = await fileToArrayBuffer(file);
const requestConfig = {
headers: {
'Content-Type': 'text/octet-stream'
onProgress(fileId, e.loaded, e.total, Date.now());
}
};
- return this.webdavClient.put(fileURL, fileContent, requestConfig);
-
+ return this.webdavClient.upload(fileURL, '', [file], requestConfig);
}
update(uuid: string, data: Partial<CollectionResource>) {
UNIQUE_VIOLATION = 'UniqueViolation',
OWNERSHIP_CYCLE = 'OwnershipCycle',
MODIFYING_CONTAINER_REQUEST_FINAL_STATE = 'ModifyingContainerRequestFinalState',
+ NAME_HAS_ALREADY_BEEN_TAKEN = 'NameHasAlreadyBeenTaken',
UNKNOWN = 'Unknown',
NONE = 'None'
}
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;
}
const { items, ...res } = response;
const mappedItems = items.map((item: GroupContentsResource) => {
const mappedItem = TrashableResourceService.mapKeys(_.camelCase)(item);
- if (item.kind === ResourceKind.COLLECTION) {
+ if (item.kind === ResourceKind.COLLECTION || item.kind === ResourceKind.PROJECT) {
const { properties } = item;
return { ...mappedItem, properties };
} else {
import { GroupClass } from "~/models/group";
import { ListArguments } from "~/services/common-service/common-resource-service";
import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
-
+import { TrashableResourceService } from '~/services/common-service/trashable-resource-service';
+import { snakeCase } from 'lodash';
export class ProjectService extends GroupsService<ProjectResource> {
create(data: Partial<ProjectResource>) {
return super.create(projectData);
}
+ update(uuid: string, data: Partial<ProjectResource>) {
+ if (uuid && data && data.properties) {
+ const { properties } = data;
+ const mappedData = {
+ ...TrashableResourceService.mapKeys(snakeCase)(data),
+ properties,
+ };
+ return TrashableResourceService
+ .defaultResponse(
+ this.serverApi
+ .put<ProjectResource>(this.resourceType + uuid, mappedData),
+ this.actions,
+ false
+ );
+ }
+ return TrashableResourceService
+ .defaultResponse(
+ this.serverApi
+ .put<ProjectResource>(this.resourceType + uuid, data && TrashableResourceService.mapKeys(snakeCase)(data)),
+ this.actions
+ );
+ }
+
list(args: ListArguments = {}) {
return super.list({
...args,
--- /dev/null
+// 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
import { SearchService } from '~/services/search-service/search-service';
import { PermissionService } from "~/services/permission-service/permission-service";
import { VirtualMachinesService } from "~/services/virtual-machines-service/virtual-machines-service";
+import { RepositoriesService } from '~/services/repositories-service/repositories-service';
+import { AuthorizedKeysService } from '~/services/authorized-keys-service/authorized-keys-service';
export type ServiceRepository = ReturnType<typeof createServices>;
const webdavClient = new WebDAV();
webdavClient.defaults.baseURL = config.keepWebServiceUrl;
+ const authorizedKeysService = new AuthorizedKeysService(apiClient, actions);
const containerRequestService = new ContainerRequestService(apiClient, actions);
const containerService = new ContainerService(apiClient, actions);
const groupsService = new GroupsService(apiClient, actions);
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 virtualMachineService = new VirtualMachinesService(apiClient, actions);
const workflowService = new WorkflowService(apiClient, actions);
ancestorsService,
apiClient,
authService,
+ authorizedKeysService,
collectionFilesService,
collectionService,
containerRequestService,
logService,
permissionService,
projectService,
+ repositoriesService,
searchService,
tagService,
userService,
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';
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,
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),
};
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;
"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
import { ofType, unionize, UnionOf } from '~/common/unionize';
import { Dispatch } from "redux";
-import { User } from "~/models/user";
+import { reset, stopSubmit } from 'redux-form';
+import { AxiosInstance } from "axios";
import { RootState } from "../store";
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
import { ServiceRepository } from "~/services/services";
-import { AxiosInstance } from "axios";
+import { getAuthorizedKeysServiceError, AuthorizedKeysServiceError } from '~/services/authorized-keys-service/authorized-keys-service';
+import { KeyType, SshKeyResource } from '~/models/ssh-key';
+import { User } from "~/models/user";
export const authActions = unionize({
SAVE_API_TOKEN: ofType<string>(),
LOGOUT: {},
INIT: ofType<{ user: User, token: string }>(),
USER_DETAILS_REQUEST: {},
- USER_DETAILS_SUCCESS: ofType<User>()
+ USER_DETAILS_SUCCESS: ofType<User>(),
+ SET_SSH_KEYS: ofType<SshKeyResource[]>(),
+ ADD_SSH_KEY: ofType<SshKeyResource>()
});
+export const SSH_KEY_CREATE_FORM_NAME = 'sshKeyCreateFormName';
+
+export interface SshKeyCreateFormDialogData {
+ publicKey: string;
+ name: string;
+}
+
function setAuthorizationHeader(services: ServiceRepository, token: string) {
services.apiClient.defaults.headers.common = {
Authorization: `OAuth2 ${token}`
});
};
+export const openSshKeyCreateDialog = () => dialogActions.OPEN_DIALOG({ id: SSH_KEY_CREATE_FORM_NAME, data: {} });
+
+export const createSshKey = (data: SshKeyCreateFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const userUuid = getState().auth.user!.uuid;
+ const { name, publicKey } = data;
+ const newSshKey = await services.authorizedKeysService.create({
+ name,
+ publicKey,
+ keyType: KeyType.SSH,
+ authorizedUserUuid: userUuid
+ });
+ dispatch(dialogActions.CLOSE_DIALOG({ id: SSH_KEY_CREATE_FORM_NAME }));
+ dispatch(reset(SSH_KEY_CREATE_FORM_NAME));
+ dispatch(authActions.ADD_SSH_KEY(newSshKey));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Public key has been successfully created.",
+ hideDuration: 2000
+ }));
+ } catch (e) {
+ const error = getAuthorizedKeysServiceError(e);
+ if (error === AuthorizedKeysServiceError.UNIQUE_PUBLIC_KEY) {
+ dispatch(stopSubmit(SSH_KEY_CREATE_FORM_NAME, { publicKey: 'Public key already exists.' }));
+ } else if (error === AuthorizedKeysServiceError.INVALID_PUBLIC_KEY) {
+ dispatch(stopSubmit(SSH_KEY_CREATE_FORM_NAME, { publicKey: 'Public key is invalid' }));
+ }
+ }
+ };
+
+export const loadSshKeysPanel = () =>
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ dispatch(setBreadcrumbs([{ label: 'SSH Keys'}]));
+ const response = await services.authorizedKeysService.list();
+ dispatch(authActions.SET_SSH_KEYS(response.items));
+ } catch (e) {
+ return;
+ }
+ };
+
+
export type AuthAction = UnionOf<typeof authActions>;
const state = reducer(initialState, authActions.SAVE_API_TOKEN("token"));
expect(state).toEqual({
apiToken: "token",
- user: undefined
+ user: undefined,
+ sshKeys: []
});
});
const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
expect(state).toEqual({
apiToken: undefined,
+ sshKeys: [],
user: {
email: "test@test.com",
firstName: "John",
import { authActions, AuthAction } from "./auth-action";
import { User } from "~/models/user";
import { ServiceRepository } from "~/services/services";
+import { SshKeyResource } from '~/models/ssh-key';
export interface AuthState {
user?: User;
apiToken?: string;
+ sshKeys?: SshKeyResource[];
}
-export const authReducer = (services: ServiceRepository) => (state: AuthState = {}, action: AuthAction) => {
+const initialState: AuthState = {
+ user: undefined,
+ apiToken: undefined,
+ sshKeys: []
+};
+
+export const authReducer = (services: ServiceRepository) => (state: AuthState = initialState, action: AuthAction) => {
return authActions.match(action, {
SAVE_API_TOKEN: (token: string) => {
return {...state, apiToken: token};
USER_DETAILS_SUCCESS: (user: User) => {
return {...state, user};
},
+ SET_SSH_KEYS: (sshKeys: SshKeyResource[]) => {
+ return {...state, sshKeys};
+ },
+ ADD_SSH_KEY: (sshKey: SshKeyResource) => {
+ return { ...state, sshKeys: state.sshKeys!.concat(sshKey) };
+ },
default: () => state
});
};
import { CollectionFilesTree, CollectionFileType } from "~/models/collection-file";
import { ServiceRepository } from "~/services/services";
import { RootState } from "../../store";
-import { snackbarActions } from "../../snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "../../snackbar/snackbar-actions";
import { dialogActions } from '../../dialog/dialog-actions';
import { getNodeValue } from "~/models/tree";
import { filterCollectionFilesBySelection } from './collection-panel-files-state';
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const currentCollection = getState().collectionPanel.item;
if (currentCollection) {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
- await services.collectionService.deleteFiles(currentCollection.uuid, filePaths);
- dispatch<any>(loadCollectionFiles(currentCollection.uuid));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing...' }));
+ try {
+ await services.collectionService.deleteFiles('', filePaths);
+ dispatch<any>(loadCollectionFiles(currentCollection.uuid));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not remove file.',
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR
+ }));
+ }
}
};
import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
export const openCollectionCopyDialog = (resource: { name: string, uuid: string }) =>
(dispatch: Dispatch) => {
dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(COLLECTION_COPY_FORM_NAME));
const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
dispatch<any>(initialize(COLLECTION_COPY_FORM_NAME, initialData));
dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_COPY_FORM_NAME, data: {} }));
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
export const COLLECTION_MOVE_FORM_NAME = 'collectionMoveFormName';
export const openMoveCollectionDialog = (resource: { name: string, uuid: string }) =>
(dispatch: Dispatch) => {
dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(COLLECTION_MOVE_FORM_NAME));
dispatch(initialize(COLLECTION_MOVE_FORM_NAME, resource));
dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_MOVE_FORM_NAME, data: {} }));
};
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
};
dispatch(initialize(COLLECTION_PARTIAL_COPY_FORM_NAME, initialData));
dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_COPY_FORM_NAME));
dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME, data: {} }));
}
};
hideDuration: 2000,
kind: SnackbarKind.ERROR
}));
- dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
}
}
};
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 }>(),
kind: ResourceKind,
menuKind: ContextMenuKind;
isTrashed?: boolean;
+ index?: number
};
export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) =>
event.nativeEvent.detail === 0;
}));
};
+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);
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from '~/common/unionize';
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { getResource } from '~/store/resources/resources';
+import { ProjectResource } from "~/models/project";
+import { ServiceRepository } from '~/services/services';
+import { TagProperty } from '~/models/tag';
+import { startSubmit, stopSubmit } from 'redux-form';
+import { resourcesActions } from '~/store/resources/resources-actions';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export const SLIDE_TIMEOUT = 500;
export const detailsPanelActions = unionize({
TOGGLE_DETAILS_PANEL: ofType<{}>(),
export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
-export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
+export const PROJECT_PROPERTIES_FORM_NAME = 'projectPropertiesFormName';
+export const PROJECT_PROPERTIES_DIALOG_NAME = 'projectPropertiesDialogName';
+export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
+export const openProjectPropertiesDialog = () =>
+ (dispatch: Dispatch) => {
+ dispatch<any>(dialogActions.OPEN_DIALOG({ id: PROJECT_PROPERTIES_DIALOG_NAME, data: { } }));
+ };
+export const deleteProjectProperty = (key: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { detailsPanel, resources } = getState();
+ const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource;
+ try {
+ if (project) {
+ delete project.properties[key];
+ const updatedProject = await services.projectService.update(project.uuid, project);
+ dispatch(resourcesActions.SET_RESOURCES([updatedProject]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully deleted.", hideDuration: 2000 }));
+ }
+ } catch (e) {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_PROPERTIES_FORM_NAME }));
+ throw new Error('Could not remove property from the project.');
+ }
+ };
+export const createProjectProperty = (data: TagProperty) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { detailsPanel, resources } = getState();
+ const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource;
+ dispatch(startSubmit(PROJECT_PROPERTIES_FORM_NAME));
+ try {
+ if (project) {
+ project.properties[data.key] = data.value;
+ const updatedProject = await services.projectService.update(project.uuid, project);
+ dispatch(resourcesActions.SET_RESOURCES([updatedProject]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully added.", hideDuration: 2000 }));
+ dispatch(stopSubmit(PROJECT_PROPERTIES_FORM_NAME));
+ }
+ return;
+ } catch (e) {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_PROPERTIES_FORM_NAME }));
+ throw new Error('Could not add property to the project.');
+ }
+ };
+export const toggleDetailsPanel = () => (dispatch: Dispatch) => {
+ // because of material-ui issue resizing details panel breaks tabs.
+ // triggering window resize event fixes that.
+ setTimeout(() => {
+ window.dispatchEvent(new Event('resize'));
+ }, SLIDE_TIMEOUT);
+ dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+};
export const navigateToSearchResults = push(Routes.SEARCH_RESULTS);
export const navigateToVirtualMachines = push(Routes.VIRTUAL_MACHINES);
+
+export const navigateToRepositories = push(Routes.REPOSITORIES);
+
+export const navigateToSshKeys= push(Routes.SSH_KEYS);
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
import { getProcess, ProcessStatus, getProcessStatus } from '~/store/processes/process';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
export const PROCESS_COPY_FORM_NAME = 'processCopyFormName';
const processStatus = getProcessStatus(process);
if (processStatus === ProcessStatus.DRAFT) {
dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(PROCESS_COPY_FORM_NAME));
const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
dispatch<any>(initialize(PROCESS_COPY_FORM_NAME, initialData));
dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} }));
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
import { projectPanelActions } from '~/store/project-panel/project-panel-action';
import { getProcess, getProcessStatus, ProcessStatus } from '~/store/processes/process';
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
export const PROCESS_MOVE_FORM_NAME = 'processMoveFormName';
const processStatus = getProcessStatus(process);
if (processStatus === ProcessStatus.DRAFT) {
dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(PROCESS_MOVE_FORM_NAME));
dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource));
dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} }));
} else {
import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
export const PROJECT_MOVE_FORM_NAME = 'projectMoveFormName';
export const openMoveProjectDialog = (resource: { name: string, uuid: string }) =>
(dispatch: Dispatch) => {
dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(PROJECT_MOVE_FORM_NAME));
dispatch(initialize(PROJECT_MOVE_FORM_NAME, resource));
dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_MOVE_FORM_NAME, data: {} }));
};
--- /dev/null
+// 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
--- /dev/null
+// 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
dispatch(dialogActions.OPEN_DIALOG({
id: SET_WORKFLOW_DIALOG,
data: {
- title: 'Data loss warning',
+ title: 'Form will be cleared',
text: 'Changing a workflow will clean all input fields in next step.',
confirmButtonLabel: 'Change Workflow',
workflow
import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service';
import { resourcesDataReducer } from "~/store/resources-data/resources-data-reducer";
import { virtualMachinesReducer } from "~/store/virtual-machines/virtual-machines-reducer";
+import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
runProcessPanel: runProcessPanelReducer,
appInfo: appInfoReducer,
searchBar: searchBarReducer,
- virtualMachines: virtualMachinesReducer
+ virtualMachines: virtualMachinesReducer,
+ repositories: repositoriesReducer
});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+
+export interface PickerIdProp {
+ pickerId: string;
+}
+
+export const pickerId =
+ (id: string) =>
+ <P extends PickerIdProp>(Component: React.ComponentType<P>) =>
+ (props: P) =>
+ <Component {...props} pickerId={id} />;
+
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from "~/common/unionize";
-import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId } from '~/models/tree';
+import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId, Tree } from '~/models/tree';
import { Dispatch } from 'redux';
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { pipe, values } from 'lodash/fp';
import { ResourceKind } from '~/models/resource';
import { GroupContentsResource } from '~/services/groups-service/groups-service';
-import { CollectionDirectory, CollectionFile } from '~/models/collection-file';
import { getTreePicker, TreePicker } from './tree-picker';
import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
import { OrderBuilder } from '~/services/api/order-builder';
import { ProjectResource } from '~/models/project';
+import { mapTree } from '../../models/tree';
export const treePickerActions = unionize({
LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
+ APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
)();
export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
getAllNodes<Value>(pickerId, node => node.selected)(state);
-
+
export const initProjectsTreePicker = (pickerId: string) =>
async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
const { home, shared, favorites } = getProjectsTreePickerIds(pickerId);
const node = getNode(id)(picker);
if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
- const files = await services.collectionService.files(node.value.portableDataHash);
- const data = getNodeDescendants('')(files).map(node => node.value);
-
- dispatch<any>(receiveTreePickerData<CollectionDirectory | CollectionFile>({
- id,
- pickerId,
- data,
- extractNodeData: value => ({
- id: value.id,
- status: TreeNodeStatus.LOADED,
- value,
- }),
- }));
+ const filesTree = await services.collectionService.files(node.value.portableDataHash);
+
+ dispatch(
+ treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
+ id,
+ pickerId,
+ subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
+ }));
+
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
}
}
};
}
};
-
+export const SHARED_PROJECT_ID = 'Shared with me';
export const initSharedProject = (pickerId: string) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
dispatch(receiveTreePickerData({
id: '',
pickerId,
- data: [{ uuid: 'Shared with me', name: 'Shared with me' }],
+ data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
extractNodeData: value => ({
id: value.uuid,
status: TreeNodeStatus.INITIAL,
}));
};
+export const FAVORITES_PROJECT_ID = 'Favorites';
export const initFavoritesProject = (pickerId: string) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
dispatch(receiveTreePickerData({
id: '',
pickerId,
- data: [{ uuid: 'Favorites', name: 'Favorites' }],
+ data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
extractNodeData: value => ({
id: value.uuid,
status: TreeNodeStatus.INITIAL,
import { compose } from "redux";
import { activateNode, getNode, toggleNodeCollapse, toggleNodeSelection } from '~/models/tree';
import { pipe } from 'lodash/fp';
+import { appendSubtree } from '~/models/tree';
export const treePickerReducer = (state: TreePicker = {}, action: TreePickerAction) =>
treePickerActions.match(action, {
LOAD_TREE_PICKER_NODE_SUCCESS: ({ id, nodes, pickerId }) =>
updateOrCreatePicker(state, pickerId, compose(receiveNodes(nodes)(id), setNodeStatus(id)(TreeNodeStatus.LOADED))),
+ APPEND_TREE_PICKER_NODE_SUBTREE: ({ id, subtree, pickerId}) =>
+ updateOrCreatePicker(state, pickerId, compose(appendSubtree(id, subtree), setNodeStatus(id)(TreeNodeStatus.LOADED))),
+
TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ id, pickerId }) =>
updateOrCreatePicker(state, pickerId, toggleNodeCollapse(id)),
// 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, setBreadcrumbs } 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';
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 { loadSshKeysPanel } from '~/store/auth/auth-action';
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';
import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/search-results-panel/search-results-panel-actions';
import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view';
import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machines-actions';
+import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions';
export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
await dispatch(loadVirtualMachinesPanel());
dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
});
+
+export const loadRepositories = handleFirstTimeLoad(
+ async (dispatch: Dispatch<any>) => {
+ await dispatch(loadRepositoriesPanel());
+ dispatch(setBreadcrumbs([{ label: 'Repositories' }]));
+ });
+
+export const loadSshKeys = handleFirstTimeLoad(
+ async (dispatch: Dispatch<any>) => {
+ await dispatch(loadSshKeysPanel());
+ });
const finishLoadingProject = (project: GroupContentsResource | string) =>
async (dispatch: Dispatch<any>) => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+
+const ERROR_MESSAGE = 'Public key is invalid';
+
+export const isRsaKey = (value: any) => {
+ return value.match(/ssh-rsa AAAA[0-9A-Za-z+/]+[=]{0,3} ([^@]+@[^@]+)/i) ? undefined : ERROR_MESSAGE;
+};
import { require } from './require';
import { maxLength } from './max-length';
+import { isRsaKey } from './is-rsa-key';
export const TAG_KEY_VALIDATION = [require, maxLength(255)];
export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
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)];
+
+export const SSH_KEY_PUBLIC_VALIDATION = [require, isRsaKey, maxLength(1024)];
+export const SSH_KEY_NAME_VALIDATION = [require, maxLength(255)];
</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)}
import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
export const collectionActionSet: ContextMenuActionSet = [[
{
icon: DetailsIcon,
name: "View details",
execute: dispatch => {
- dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+ dispatch<any>(toggleDetailsPanel());
}
},
// {
import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
export const collectionResourceActionSet: ContextMenuActionSet = [[
{
icon: DetailsIcon,
name: "View details",
execute: dispatch => {
- dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+ dispatch<any>(toggleDetailsPanel());
}
},
{
import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
import { openProcessInputDialog } from "~/store/processes/process-input-actions";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
export const processActionSet: ContextMenuActionSet = [[
{
icon: DetailsIcon,
name: "View details",
execute: dispatch => {
- dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+ dispatch<any>(toggleDetailsPanel());
}
},
// {
import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
-import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
import { openRemoveProcessDialog } from "~/store/processes/processes-actions";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
export const processResourceActionSet: ContextMenuActionSet = [[
{
icon: DetailsIcon,
name: "View details",
execute: dispatch => {
- dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+ dispatch<any>(toggleDetailsPanel());
}
},
{
import { ShareIcon } from '~/components/icon/icon';
import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
export const projectActionSet: ContextMenuActionSet = [[
{
icon: DetailsIcon,
name: "View details",
execute: dispatch => {
- dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+ dispatch<any>(toggleDetailsPanel());
}
},
{
--- /dev/null
+// 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));
+ }
+}]];
import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
export const trashedCollectionActionSet: ContextMenuActionSet = [[
{
icon: DetailsIcon,
name: "View details",
execute: dispatch => {
- dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+ dispatch<any>(toggleDetailsPanel());
}
},
{
TRASHED_COLLECTION = 'TrashedCollection',
PROCESS = "Process",
PROCESS_RESOURCE = 'ProcessResource',
- PROCESS_LOGS = "ProcessLogs"
+ PROCESS_LOGS = "ProcessLogs",
+ REPOSITORY = "Repository"
}
import * as classnames from "classnames";
import { connect } from 'react-redux';
import { RootState } from '~/store/store';
-import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
import { CloseIcon } from '~/components/icon/icon';
import { EmptyResource } from '~/models/empty';
import { Dispatch } from "redux";
import { getResource } from '~/store/resources/resources';
import { ResourceData } from "~/store/resources-data/resources-data-reducer";
import { getResourceData } from "~/store/resources-data/resources-data";
+import { toggleDetailsPanel, SLIDE_TIMEOUT } from '~/store/details-panel/details-panel-action';
type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
const DRAWER_WIDTH = 320;
-const SLIDE_TIMEOUT = 500;
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
background: theme.palette.background.paper,
const mapDispatchToProps = (dispatch: Dispatch) => ({
onCloseDrawer: () => {
- dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+ dispatch<any>(toggleDetailsPanel());
}
});
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { ProjectIcon } from '~/components/icon/icon';
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import { openProjectPropertiesDialog } from '~/store/details-panel/details-panel-action';
+import { ProjectIcon, RenameIcon } from '~/components/icon/icon';
import { ProjectResource } from '~/models/project';
import { formatDate } from '~/common/formatters';
import { ResourceKind } from '~/models/resource';
import { DetailsData } from "./details-data";
import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
import { RichTextEditorLink } from '~/components/rich-text-editor-link/rich-text-editor-link';
+import { withStyles, StyleRulesCallback, Chip, WithStyles } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
export class ProjectDetails extends DetailsData<ProjectResource> {
-
getIcon(className?: string) {
return <ProjectIcon className={className} />;
}
getDetails() {
- return <div>
- <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
- {/* Missing attr */}
- <DetailsAttribute label='Size' value='---' />
- <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
- <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
- <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
- {/* Missing attr */}
- {/*<DetailsAttribute label='File size' value='1.4 GB' />*/}
- <DetailsAttribute label='Description'>
- {this.item.description ?
- <RichTextEditorLink
- title={`Description of ${this.item.name}`}
- content={this.item.description}
- label='Show full description' />
- : '---'
- }
- </DetailsAttribute>
- </div>;
+ return <ProjectDetailsComponent project={this.item} />;
+ }
+}
+
+type CssRules = 'tag' | 'editIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ tag: {
+ marginRight: theme.spacing.unit,
+ marginBottom: theme.spacing.unit
+ },
+ editIcon: {
+ fontSize: '1.125rem',
+ cursor: 'pointer'
}
+});
+
+
+interface ProjectDetailsComponentDataProps {
+ project: ProjectResource;
+}
+
+interface ProjectDetailsComponentActionProps {
+ onClick: () => void;
}
+
+const mapDispatchToProps = ({ onClick: openProjectPropertiesDialog });
+
+type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDetailsComponentActionProps & WithStyles<CssRules>;
+
+const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
+ withStyles(styles)(
+ ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
+ <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
+ {/* Missing attr */}
+ <DetailsAttribute label='Size' value='---' />
+ <DetailsAttribute label='Owner' value={project.ownerUuid} lowercaseValue={true} />
+ <DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
+ <DetailsAttribute label='Created at' value={formatDate(project.createdAt)} />
+ {/* Missing attr */}
+ {/*<DetailsAttribute label='File size' value='1.4 GB' />*/}
+ <DetailsAttribute label='Description'>
+ {project.description ?
+ <RichTextEditorLink
+ title={`Description of ${project.name}`}
+ content={project.description}
+ label='Show full description' />
+ : '---'
+ }
+ </DetailsAttribute>
+ <DetailsAttribute label='Properties'>
+ <div onClick={onClick}>
+ <RenameIcon className={classes.editIcon} />
+ </div>
+ </DetailsAttribute>
+ {
+ Object.keys(project.properties).map(k => {
+ return <Chip key={k} className={classes.tag} label={`${k}: ${project.properties[k]}`} />;
+ })
+ }
+ </div>
+));
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
+import { memoize } from "lodash/fp";
import { FormDialog } from '~/components/form-dialog/form-dialog';
import { CollectionNameField, CollectionDescriptionField, CollectionProjectPickerField } from '~/views-components/form-fields/collection-form-fields';
import { WithDialogProps } from '~/store/dialog/with-dialog';
import { InjectedFormProps } from 'redux-form';
import { CollectionPartialCopyFormData } from '~/store/collections/collection-partial-copy-actions';
+import { PickerIdProp } from "~/store/tree-picker/picker-id";
type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyFormData>;
-export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyProps) =>
+export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyProps & PickerIdProp) =>
<FormDialog
dialogTitle='Create a collection'
- formFields={CollectionPartialCopyFields}
+ formFields={CollectionPartialCopyFields(props.pickerId)}
submitLabel='Create a collection'
{...props}
/>;
-export const CollectionPartialCopyFields = () => <div>
- <CollectionNameField />
- <CollectionDescriptionField />
- <CollectionProjectPickerField />
-</div>;
+export const CollectionPartialCopyFields = memoize(
+ (pickerId: string) =>
+ () =>
+ <div>
+ <CollectionNameField />
+ <CollectionDescriptionField />
+ <CollectionProjectPickerField {...{ pickerId }} />
+ </div>);
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
+import { memoize } from 'lodash/fp';
import { InjectedFormProps, Field } from 'redux-form';
import { WithDialogProps } from '~/store/dialog/with-dialog';
import { FormDialog } from '~/components/form-dialog/form-dialog';
import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from '~/validators/validators';
import { TextField } from "~/components/text-field/text-field";
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { PickerIdProp } from '~/store/tree-picker/picker-id';
type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
-export const DialogCopy = (props: CopyFormDialogProps) =>
+export const DialogCopy = (props: CopyFormDialogProps & PickerIdProp) =>
<FormDialog
dialogTitle='Make a copy'
- formFields={CopyDialogFields}
+ formFields={CopyDialogFields(props.pickerId)}
submitLabel='Copy'
{...props}
/>;
-const CopyDialogFields = () => <span>
- <Field
- name='name'
- component={TextField}
- validate={COPY_NAME_VALIDATION}
- label="Enter a new name for the copy" />
- <Field
- name="ownerUuid"
- component={ProjectTreePickerField}
- validate={COPY_FILE_VALIDATION} />
-</span>;
+const CopyDialogFields = memoize((pickerId: string) =>
+ () =>
+ <span>
+ <Field
+ name='name'
+ component={TextField}
+ validate={COPY_NAME_VALIDATION}
+ label="Enter a new name for the copy" />
+ <Field
+ name="ownerUuid"
+ component={ProjectTreePickerField}
+ validate={COPY_FILE_VALIDATION}
+ pickerId={pickerId}/>
+ </span>);
--- /dev/null
+// 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}
+ />;
+
+
--- /dev/null
+// 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 { SshKeyPublicField, SshKeyNameField } from '~/views-components/form-fields/ssh-key-form-fields';
+import { SshKeyCreateFormDialogData } from '~/store/auth/auth-action';
+
+type DialogSshKeyProps = WithDialogProps<{}> & InjectedFormProps<SshKeyCreateFormDialogData>;
+
+export const DialogSshKeyCreate = (props: DialogSshKeyProps) =>
+ <FormDialog
+ dialogTitle='Add new SSH key'
+ formFields={SshKeyAddFields}
+ submitLabel='Add new ssh key'
+ {...props}
+ />;
+
+const SshKeyAddFields = () => <span>
+ <SshKeyPublicField />
+ <SshKeyNameField />
+</span>;
import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
import { copyCollection } from '~/store/workbench/workbench-actions';
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { pickerId } from '~/store/tree-picker/picker-id';
export const CopyCollectionDialog = compose(
withDialog(COLLECTION_COPY_FORM_NAME),
onSubmit: (data, dispatch) => {
dispatch(copyCollection(data));
}
- })
+ }),
+ pickerId(COLLECTION_COPY_FORM_NAME),
)(DialogCopy);
\ No newline at end of file
import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
import { copyProcess } from '~/store/workbench/workbench-actions';
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { pickerId } from "~/store/tree-picker/picker-id";
export const CopyProcessDialog = compose(
withDialog(PROCESS_COPY_FORM_NAME),
onSubmit: (data, dispatch) => {
dispatch(copyProcess(data));
}
- })
+ }),
+ pickerId(PROCESS_COPY_FORM_NAME),
)(DialogCopy);
\ No newline at end of file
--- /dev/null
+// 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
--- /dev/null
+// 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 { SSH_KEY_CREATE_FORM_NAME, createSshKey, SshKeyCreateFormDialogData } from '~/store/auth/auth-action';
+import { DialogSshKeyCreate } from '~/views-components/dialog-create/dialog-ssh-key-create';
+
+export const CreateSshKeyDialog = compose(
+ withDialog(SSH_KEY_CREATE_FORM_NAME),
+ reduxForm<SshKeyCreateFormDialogData>({
+ form: SSH_KEY_CREATE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(createSshKey(data));
+ }
+ })
+)(DialogSshKeyCreate);
\ No newline at end of file
import { COLLECTION_MOVE_FORM_NAME } from '~/store/collections/collection-move-actions';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { moveCollection } from '~/store/workbench/workbench-actions';
+import { pickerId } from '~/store/tree-picker/picker-id';
export const MoveCollectionDialog = compose(
withDialog(COLLECTION_MOVE_FORM_NAME),
onSubmit: (data, dispatch) => {
dispatch(moveCollection(data));
}
- })
+ }),
+ pickerId(COLLECTION_MOVE_FORM_NAME),
)(DialogMoveTo);
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
import { moveProcess } from '~/store/workbench/workbench-actions';
+import { pickerId } from '~/store/tree-picker/picker-id';
export const MoveProcessDialog = compose(
withDialog(PROCESS_MOVE_FORM_NAME),
onSubmit: (data, dispatch) => {
dispatch(moveProcess(data));
}
- })
+ }),
+ pickerId(PROCESS_MOVE_FORM_NAME),
)(DialogMoveTo);
\ No newline at end of file
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
import { moveProject } from '~/store/workbench/workbench-actions';
+import { pickerId } from '~/store/tree-picker/picker-id';
export const MoveProjectDialog = compose(
withDialog(PROJECT_MOVE_FORM_NAME),
onSubmit: (data, dispatch) => {
dispatch(moveProject(data));
}
- })
+ }),
+ pickerId(PROJECT_MOVE_FORM_NAME),
)(DialogMoveTo);
import { withDialog, } from '~/store/dialog/with-dialog';
import { CollectionPartialCopyFormData, copyCollectionPartial, COLLECTION_PARTIAL_COPY_FORM_NAME } from '~/store/collections/collection-partial-copy-actions';
import { DialogCollectionPartialCopy } from "~/views-components/dialog-copy/dialog-collection-partial-copy";
+import { pickerId } from "~/store/tree-picker/picker-id";
export const PartialCopyCollectionDialog = compose(
onSubmit: (data, dispatch) => {
dispatch(copyCollectionPartial(data));
}
- }))(DialogCollectionPartialCopy);
\ No newline at end of file
+ }),
+ pickerId(COLLECTION_PARTIAL_COPY_FORM_NAME),
+)(DialogCollectionPartialCopy);
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
+import { memoize } from 'lodash/fp';
import { InjectedFormProps, Field } from 'redux-form';
import { WithDialogProps } from '~/store/dialog/with-dialog';
import { FormDialog } from '~/components/form-dialog/form-dialog';
import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
import { MOVE_TO_VALIDATION } from '~/validators/validators';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { PickerIdProp } from "~/store/tree-picker/picker-id";
-export const DialogMoveTo = (props: WithDialogProps<string> & InjectedFormProps<MoveToFormDialogData>) =>
+export const DialogMoveTo = (props: WithDialogProps<string> & InjectedFormProps<MoveToFormDialogData> & PickerIdProp) =>
<FormDialog
dialogTitle='Move to'
- formFields={MoveToDialogFields}
+ formFields={MoveToDialogFields(props.pickerId)}
submitLabel='Move'
{...props}
/>;
-const MoveToDialogFields = () =>
- <Field
- name="ownerUuid"
- component={ProjectTreePickerField}
- validate={MOVE_TO_VALIDATION} />;
+const MoveToDialogFields = memoize(
+ (pickerId: string) => () =>
+ <Field
+ name="ownerUuid"
+ pickerId={pickerId}
+ component={ProjectTreePickerField}
+ validate={MOVE_TO_VALIDATION} />);
import { Field, WrappedFieldProps } from "redux-form";
import { TextField } from "~/components/text-field/text-field";
import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
-import { ProjectTreePicker } from "~/views-components/project-tree-picker/project-tree-picker";
+import { ProjectTreePicker, ProjectTreePickerField } from "~/views-components/project-tree-picker/project-tree-picker";
+import { PickerIdProp } from '../../store/tree-picker/picker-id';
export const CollectionNameField = () =>
<Field
validate={COLLECTION_DESCRIPTION_VALIDATION}
label="Description - optional" />;
-export const CollectionProjectPickerField = () =>
+export const CollectionProjectPickerField = (props: PickerIdProp) =>
<Field
name="projectUuid"
- component={ProjectPicker}
+ pickerId={props.pickerId}
+ component={ProjectTreePickerField}
validate={COLLECTION_PROJECT_VALIDATION} />;
-
-const ProjectPicker = (props: WrappedFieldProps) =>
- <div style={{ height: '144px', display: 'flex', flexDirection: 'column' }}>
- <ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} />
- </div>;
--- /dev/null
+// 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
--- /dev/null
+// 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 { SSH_KEY_PUBLIC_VALIDATION, SSH_KEY_NAME_VALIDATION } from "~/validators/validators";
+
+export const SshKeyPublicField = () =>
+ <Field
+ name='publicKey'
+ component={TextField}
+ validate={SSH_KEY_PUBLIC_VALIDATION}
+ autoFocus={true}
+ label="Public Key" />;
+
+export const SshKeyNameField = () =>
+ <Field
+ name='name'
+ component={TextField}
+ validate={SSH_KEY_NAME_VALIDATION}
+ label="Name" />;
+
+
import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
import { UserPanelIcon } from "~/components/icon/icon";
import { DispatchProp, connect } from 'react-redux';
-import { logout } from "~/store/auth/auth-action";
+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";
+import { navigateToSshKeys } from '~/store/navigation/navigation-action';
import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions";
interface AccountMenuProps {
{getUserFullname(user)}
</MenuItem>
<MenuItem onClick={() => dispatch(openVirtualMachines())}>Virtual Machines</MenuItem>
+ <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
<MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
+ <MenuItem onClick={() => dispatch(navigateToSshKeys)}>Ssh Keys</MenuItem>
<MenuItem>My account</MenuItem>
<MenuItem onClick={() => dispatch(logout())}>Logout</MenuItem>
</DropdownMenu>
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 { matchVirtualMachineRoute } from '~/routes/routes';
+import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute } from '~/routes/routes';
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
interface MainContentBarProps {
onDetailsPanelToggle: () => void;
return !!match;
};
+const isRepositoriesPath = ({ router }: RootState) => {
+ const pathname = router.location ? router.location.pathname : '';
+ const match = matchRepositoriesRoute(pathname);
+ return !!match;
+};
+
+const isSshKeysPath = ({ router }: RootState) => {
+ const pathname = router.location ? router.location.pathname : '';
+ const match = matchSshKeysRoute(pathname);
+ return !!match;
+};
+
export const MainContentBar = connect((state: RootState) => ({
- buttonVisible: !isWorkflowPath(state) && !isVirtualMachinePath(state)
+ buttonVisible: !isWorkflowPath(state) && !isSshKeysPath(state) && !isRepositoriesPath(state) && !isVirtualMachinePath(state)
}), {
- onDetailsPanelToggle: detailsPanelActions.TOGGLE_DETAILS_PANEL
+ onDetailsPanelToggle: toggleDetailsPanel
})((props: MainContentBarProps) =>
<Toolbar>
<Grid container>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { RootState } from '~/store/store';
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { ProjectResource } from '~/models/project';
+import { PROJECT_PROPERTIES_DIALOG_NAME, deleteProjectProperty } from '~/store/details-panel/details-panel-action';
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Chip, withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { ProjectPropertiesForm } from '~/views-components/project-properties-dialog/project-properties-form';
+import { getResource } from '~/store/resources/resources';
+
+type CssRules = 'tag';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ tag: {
+ marginRight: theme.spacing.unit,
+ marginBottom: theme.spacing.unit
+ }
+});
+
+interface ProjectPropertiesDialogDataProps {
+ project: ProjectResource;
+}
+
+interface ProjectPropertiesDialogActionProps {
+ handleDelete: (key: string) => void;
+}
+
+const mapStateToProps = ({ detailsPanel, resources }: RootState): ProjectPropertiesDialogDataProps => {
+ const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource;
+ return { project };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): ProjectPropertiesDialogActionProps => ({
+ handleDelete: (key: string) => dispatch<any>(deleteProjectProperty(key))
+});
+
+type ProjectPropertiesDialogProps = ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles<CssRules>;
+
+export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToProps)(
+ withStyles(styles)(
+ withDialog(PROJECT_PROPERTIES_DIALOG_NAME)(
+ ({ classes, open, closeDialog, handleDelete, project }: ProjectPropertiesDialogProps) =>
+ <Dialog open={open}
+ onClose={closeDialog}
+ fullWidth
+ maxWidth='sm'>
+ <DialogTitle>Properties</DialogTitle>
+ <DialogContent>
+ <ProjectPropertiesForm />
+ {project && project.properties &&
+ Object.keys(project.properties).map(k => {
+ return <Chip key={k} className={classes.tag}
+ onDelete={() => handleDelete(k)}
+ label={`${k}: ${project.properties[k]}`} />;
+ })
+ }
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+)));
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field, reset } from 'redux-form';
+import { compose, Dispatch } from 'redux';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '@material-ui/core';
+import { TagProperty } from '~/models/tag';
+import { TextField } from '~/components/text-field/text-field';
+import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '~/validators/validators';
+import { PROJECT_PROPERTIES_FORM_NAME, createProjectProperty } from '~/store/details-panel/details-panel-action';
+
+type CssRules = 'root' | 'keyField' | 'valueField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ display: 'flex'
+ },
+ keyField: {
+ width: '40%',
+ marginRight: theme.spacing.unit * 3
+ },
+ valueField: {
+ width: '40%',
+ marginRight: theme.spacing.unit * 3
+ },
+ buttonWrapper: {
+ paddingTop: '14px',
+ position: 'relative',
+ },
+ saveButton: {
+ boxShadow: 'none'
+ },
+ circularProgress: {
+ position: 'absolute',
+ top: -9,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ margin: 'auto'
+ }
+});
+
+interface ProjectPropertiesFormDataProps {
+ submitting: boolean;
+ invalid: boolean;
+ pristine: boolean;
+}
+
+interface ProjectPropertiesFormActionProps {
+ handleSubmit: any;
+}
+
+type ProjectPropertiesFormProps = ProjectPropertiesFormDataProps & ProjectPropertiesFormActionProps & WithStyles<CssRules>;
+
+export const ProjectPropertiesForm = compose(
+ reduxForm({
+ form: PROJECT_PROPERTIES_FORM_NAME,
+ onSubmit: (data: TagProperty, dispatch: Dispatch) => {
+ dispatch<any>(createProjectProperty(data));
+ dispatch(reset(PROJECT_PROPERTIES_FORM_NAME));
+ }
+ }),
+ withStyles(styles))(
+ ({ classes, submitting, pristine, invalid, handleSubmit }: ProjectPropertiesFormProps) =>
+ <form onSubmit={handleSubmit} className={classes.root}>
+ <div className={classes.keyField}>
+ <Field name="key"
+ disabled={submitting}
+ component={TextField}
+ validate={TAG_KEY_VALIDATION}
+ label="Key" />
+ </div>
+ <div className={classes.valueField}>
+ <Field name="value"
+ disabled={submitting}
+ component={TextField}
+ validate={TAG_VALUE_VALIDATION}
+ label="Value" />
+ </div>
+ <div className={classes.buttonWrapper}>
+ <Button type="submit" className={classes.saveButton}
+ color="primary"
+ size='small'
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ ADD
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+ </div>
+ </form>
+ );
import { ServiceRepository } from "~/services/services";
import { WrappedFieldProps } from 'redux-form';
import { TreePickerId } from '~/models/tree';
+import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
+import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
+import { PickerIdProp } from '~/store/tree-picker/picker-id';
type ProjectTreePickerProps = Pick<TreePickerProps<ProjectResource>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
isActive={item.active}
hasMargin={true} />;
-export const ProjectTreePickerField = (props: WrappedFieldProps) =>
+export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
<div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
- <ProjectTreePicker onChange={handleChange(props)} />
+ <ProjectsTreePicker
+ pickerId={props.pickerId}
+ toggleItemActive={handleChange(props)} />
{props.meta.dirty && props.meta.error &&
<Typography variant='caption' color='error'>
{props.meta.error}
</Typography>}
</div>;
-const handleChange = (props: WrappedFieldProps) => (value: string) =>
- props.input.value === value
- ? props.input.onChange('')
- : props.input.onChange(value);
-
+const handleChange = (props: WrappedFieldProps) =>
+ (_: any, { id }: TreeItem<ProjectsTreePickerItem>) =>
+ props.input.onChange(id);
import * as React from "react";
import { Dispatch } from "redux";
import { connect } from "react-redux";
+import { isEqual } from 'lodash/fp';
import { TreeItem, TreeItemStatus } from '~/components/tree/tree';
import { ProjectResource } from "~/models/project";
import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
rootItemIcon: IconType;
showSelection?: boolean;
relatedTreePickers?: string[];
+ disableActivation?: string[];
loadRootItem: (item: TreeItem<ProjectsTreePickerRootItem>, pickerId: string, includeCollections?: boolean, inlcudeFiles?: boolean) => void;
}
const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeFiles, relatedTreePickers, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
onContextMenu: () => { return; },
toggleItemActive: (event, item, pickerId) => {
+
+ const { disableActivation = [] } = props;
+ if(disableActivation.some(isEqual(item.id))){
+ return;
+ }
+
dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: item.id, pickerId, relatedTreePickers }));
if (props.toggleItemActive) {
props.toggleItemActive(event, item, pickerId);
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { values, memoize, pipe } from 'lodash/fp';
+import { values, memoize, pipe, pick } from 'lodash/fp';
import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker';
import { SharedTreePicker } from '~/views-components/projects-tree-picker/shared-tree-picker';
import { FavoritesTreePicker } from '~/views-components/projects-tree-picker/favorites-tree-picker';
-import { getProjectsTreePickerIds } from '~/store/tree-picker/tree-picker-actions';
+import { getProjectsTreePickerIds, SHARED_PROJECT_ID, FAVORITES_PROJECT_ID } from '~/store/tree-picker/tree-picker-actions';
import { TreeItem } from '~/components/tree/tree';
import { ProjectsTreePickerItem } from './generic-projects-tree-picker';
export const ProjectsTreePicker = ({ pickerId, ...props }: ProjectsTreePickerProps) => {
const { home, shared, favorites } = getProjectsTreePickerIds(pickerId);
const relatedTreePickers = getRelatedTreePickers(pickerId);
+ const p = {
+ ...props,
+ relatedTreePickers,
+ disableActivation
+ };
return <div>
- <HomeTreePicker pickerId={home} {...props} {...{ relatedTreePickers }} />
- <SharedTreePicker pickerId={shared} {...props} {...{ relatedTreePickers }} />
- <FavoritesTreePicker pickerId={favorites} {...props} {...{ relatedTreePickers }} />
+ <HomeTreePicker pickerId={home} {...p} />
+ <SharedTreePicker pickerId={shared} {...p} />
+ <FavoritesTreePicker pickerId={favorites} {...p} />
</div>;
};
const getRelatedTreePickers = memoize(pipe(getProjectsTreePickerIds, values));
+const disableActivation = [SHARED_PROJECT_ID, FAVORITES_PROJECT_ID];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from "@material-ui/core";
+import { WithDialogProps } from "~/store/dialog/with-dialog";
+import { withDialog } from '~/store/dialog/with-dialog';
+import { 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
+`;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, 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>
+ );
+};
--- /dev/null
+// 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
// 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';
--- /dev/null
+// 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';
+import { Routes } from '~/routes/routes';
+
+
+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={Routes.SSH_KEYS} 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
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Button, Typography } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { SshKeyResource } from '~/models/ssh-key';
+
+
+type CssRules = 'root' | 'link';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%'
+ },
+ link: {
+ color: theme.palette.primary.main,
+ textDecoration: 'none',
+ margin: '0px 4px'
+ }
+});
+
+export interface SshKeyPanelRootActionProps {
+ onClick: () => void;
+}
+
+export interface SshKeyPanelRootDataProps {
+ sshKeys?: SshKeyResource[];
+}
+
+type SshKeyPanelRootProps = SshKeyPanelRootDataProps & SshKeyPanelRootActionProps & WithStyles<CssRules>;
+
+export const SshKeyPanelRoot = withStyles(styles)(
+ ({ classes, sshKeys, onClick }: SshKeyPanelRootProps) =>
+ <Card className={classes.root}>
+ <CardContent>
+ <Typography variant='body1' paragraph={true}>
+ You have not yet set up an SSH public key for use with Arvados.
+ <a href='https://doc.arvados.org/user/getting_started/ssh-access-unix.html' target='blank' className={classes.link}>
+ Learn more.
+ </a>
+ </Typography>
+ <Typography variant='body1' paragraph={true}>
+ When you have an SSH key you would like to use, add it using button below.
+ </Typography>
+ <Button
+ onClick={onClick}
+ color="primary"
+ variant="contained">
+ Add New Ssh Key
+ </Button>
+ </CardContent>
+ </Card>
+ );
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { SshKeyPanelRoot, SshKeyPanelRootDataProps, SshKeyPanelRootActionProps } from '~/views/ssh-key-panel/ssh-key-panel-root';
+import { openSshKeyCreateDialog } from '~/store/auth/auth-action';
+
+const mapStateToProps = (state: RootState): SshKeyPanelRootDataProps => {
+ return {
+ sshKeys: state.auth.sshKeys
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): SshKeyPanelRootActionProps => ({
+ onClick: () => {
+ dispatch(openSshKeyCreateDialog());
+ }
+});
+
+export const SshKeyPanel = connect(mapStateToProps, mapDispatchToProps)(SshKeyPanelRoot);
\ No newline at end of file
import SplitterLayout from 'react-splitter-layout';
import { WorkflowPanel } from '~/views/workflow-panel/workflow-panel';
import { SearchResultsPanel } from '~/views/search-results-panel/search-results-panel';
+import { SshKeyPanel } from '~/views/ssh-key-panel/ssh-key-panel';
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 { VirtualMachinePanel } from '~/views/virtual-machine-panel/virtual-machine-panel';
+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';
+import { CreateSshKeyDialog } from '~/views-components/dialog-forms/create-ssh-key-dialog';
type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
<Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
<Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
<Route path={Routes.VIRTUAL_MACHINES} component={VirtualMachinePanel} />
+ <Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
+ <Route path={Routes.SSH_KEYS} component={SshKeyPanel} />
</Switch>
</Grid>
</Grid>
<CopyProcessDialog />
<CreateCollectionDialog />
<CreateProjectDialog />
+ <CreateRepositoryDialog />
+ <CreateSshKeyDialog />
<CurrentTokenDialog />
<FileRemoveDialog />
<FilesUploadCollectionDialog />
<PartialCopyCollectionDialog />
<ProcessCommandDialog />
<ProcessInputDialog />
+ <ProjectPropertiesDialog />
<RemoveProcessDialog />
+ <RemoveRepositoryDialog />
<RenameFileDialog />
+ <RepositoryAttributesDialog />
+ <RepositoriesSampleGitDialog />
<RichTextEditorDialog />
<SharingDialog />
<Snackbar />