cy.get('[data-cy=invite-people-field] input').type("other");
});
cy.get('[role=tooltip]').click();
+ // Add admin to the group
+ cy.get('.sharing-dialog')
+ .should('contain', 'Sharing settings')
+ .within(() => {
+ cy.get('[data-cy=invite-people-field] input').type("admin");
+ });
+ cy.get('[role=tooltip]').click();
cy.get('.sharing-dialog').contains('Save').click();
cy.get('.sharing-dialog').contains('Close').click();
.within(() => {
cy.contains('Write');
});
+
+ // Change admin to manage
+ cy.get('[data-cy=group-members-data-explorer]')
+ .contains(adminUser.user.full_name)
+ .parents('tr')
+ .within(() => {
+ cy.contains('Read')
+ .parents('td')
+ .within(() => {
+ cy.get('button').click();
+ });
+ });
+ cy.get('[data-cy=context-menu]')
+ .contains('Manage')
+ .click();
+ cy.get('[data-cy=group-members-data-explorer]')
+ .contains(adminUser.user.full_name)
+ .parents('tr')
+ .within(() => {
+ cy.contains('Manage');
+ });
});
it('can unhide and re-hide users', function() {
});
it('renames the group', function() {
+ cy.loginAs(adminUser);
// Navigate to Groups
cy.get('[data-cy=side-panel-tree]').contains('Groups').click();
});
});
+
+ it('allows copying processes', function() {
+ const crName = 'first_container_request';
+ const copiedCrName = 'copied_container_request';
+ createContainerRequest(
+ activeUser,
+ crName,
+ 'arvados/jobs',
+ ['echo', 'hello world'],
+ false, 'Committed')
+ .then(function(containerRequest) {
+ cy.loginAs(activeUser);
+ cy.goToPath(`/processes/${containerRequest.uuid}`);
+ cy.get('[data-cy=process-details]').should('contain', crName);
+
+ cy.get('[data-cy=process-details]').find('button[title="More options"]').click();
+ cy.get('ul[data-cy=context-menu]').contains("Copy and re-run process").click();
+ });
+
+ cy.get('[data-cy=form-dialog]').within(() => {
+ cy.get('input[name=name]').clear().type(copiedCrName);
+ cy.get('[data-cy=projects-tree-home-tree-picker]').click();
+ cy.get('[data-cy=form-submit-btn]').click();
+ });
+
+ cy.get('[data-cy=process-details]').should('contain', copiedCrName);
+ cy.get('[data-cy=process-details]').find('button').contains('Run Process');
+ });
+
});
Workbench2: {
ExternalURL: string;
};
+ Workbench: {
+ DisableSharingURLsUI: boolean;
+ ArvadosDocsite: string;
+ FileViewersConfigURL: string;
+ WelcomePageHTML: string;
+ InactivePageHTML: string;
+ SSHHelpPageHTML: string;
+ SSHHelpHostSuffix: string;
+ SiteName: string;
+ IdleTimeout: string;
+ };
Websocket: {
ExternalURL: string;
};
SSHHelpHostSuffix: string;
SiteName: string;
IdleTimeout: string;
+ BannerUUID: string;
};
Login: {
LoginCluster: string;
WebDAV: { ExternalURL: '' },
WebDAVDownload: { ExternalURL: '' },
WebShell: { ExternalURL: '' },
+ Workbench: {
+ DisableSharingURLsUI: false,
+ ArvadosDocsite: "",
+ FileViewersConfigURL: "",
+ WelcomePageHTML: "",
+ InactivePageHTML: "",
+ SSHHelpPageHTML: "",
+ SSHHelpHostSuffix: "",
+ SiteName: "",
+ IdleTimeout: "0s"
+ }
},
Workbench: {
DisableSharingURLsUI: false,
SSHHelpHostSuffix: '',
SiteName: '',
IdleTimeout: '0s',
+ BannerUUID: ""
},
Login: {
LoginCluster: '',
import HelpOutline from '@material-ui/icons/HelpOutline';
import History from '@material-ui/icons/History';
import Inbox from '@material-ui/icons/Inbox';
-import Memory from '@material-ui/icons/Memory';
+import Memory from '@material-ui/icons/Memory';
import MoveToInbox from '@material-ui/icons/MoveToInbox';
import Info from '@material-ui/icons/Info';
import Input from '@material-ui/icons/Input';
export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
export const MailIcon: IconType = (props) => <Mail {...props} />;
export const MaximizeIcon: IconType = (props) => <FullscreenSharp {...props} />;
-export const MemoryIcon: IconType = (props) => <Memory {...props} />;
+export const MemoryIcon: IconType = (props) => <Memory {...props} />;
export const UnMaximizeIcon: IconType = (props) => <FullscreenExitSharp {...props} />;
export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
export const MoveToIcon: IconType = (props) => <Input {...props} />;
export const SetupIcon: IconType = (props) => <RemoveCircleOutline {...props} />;
export const InactiveIcon: IconType = (props) => <NotInterested {...props} />;
export const ImageIcon: IconType = (props) => <Image {...props} />;
+export const StartIcon: IconType = (props) => <PlayArrow {...props} />;
width: 100vw;
height: 100vh;
}
+
+.app-banner {
+ width: calc(100% - 2rem);
+ height: 150px;
+ z-index: 11111;
+ position: fixed;
+ top: 0px;
+ background-color: #00bfa5;
+ border: 1px solid #01685a;
+ color: #ffffff;
+ margin: 1rem;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.app-banner span {
+ font-size: 2rem;
+ text-align: center;
+ display: block;
+ margin: auto;
+ padding: 2rem;
+}
\ No newline at end of file
name: string;
outputName: string;
outputPath: string;
+ outputProperties: any;
+ outputStorageClasses: string[];
outputTtl: number;
outputUuid: string | null;
priority: number | null;
return CommonService.defaultResponse(
this.serverApi.get(`/${this.resourceType}`, { params }),
this.actions,
+ true,
showErrors
);
} else {
return CommonService.defaultResponse(
this.serverApi.post(`/${this.resourceType}`, formData, {}),
this.actions,
+ true,
showErrors
);
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from "store/store";
+import { unionize, UnionOf } from 'common/unionize';
+
+export const bannerReducerActions = unionize({
+ OPEN_BANNER: {},
+ CLOSE_BANNER: {},
+});
+
+export type BannerAction = UnionOf<typeof bannerReducerActions>;
+
+export const openBanner = () =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch(bannerReducerActions.OPEN_BANNER());
+ };
+
+export const closeBanner = () =>
+ async (dispatch: Dispatch<any>, getState: () => RootState) => {
+ dispatch(bannerReducerActions.CLOSE_BANNER());
+ };
+
+export default {
+ openBanner,
+ closeBanner
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { BannerAction, bannerReducerActions } from "./banner-action";
+
+export interface BannerState {
+ isOpen: boolean;
+}
+
+const initialState = {
+ isOpen: false,
+};
+
+export const bannerReducer = (state: BannerState = initialState, action: BannerAction) =>
+ bannerReducerActions.match(action, {
+ default: () => state,
+ OPEN_BANNER: () => ({
+ ...state,
+ isOpen: true,
+ }),
+ CLOSE_BANNER: () => ({
+ ...state,
+ isOpen: false,
+ }),
+ });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import { copyProcess } from './process-copy-actions';
+import { CommonService } from 'services/common-service/common-service';
+import { snakeCase } from 'lodash';
+
+configure({ adapter: new Adapter() });
+
+describe('ProcessCopyAction', () => {
+ // let props;
+ let dispatch: any, getState: any, services: any;
+
+ let sampleFailedProcess = {
+ command: [
+ "arvados-cwl-runner",
+ "--api=containers",
+ "--local",
+ "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
+ "/var/lib/cwl/workflow.json#main",
+ "/var/lib/cwl/cwl.input.json",
+ ],
+ container_count: 1,
+ container_count_max: 10,
+ container_image: "arvados/jobs",
+ container_uuid: "zzzzz-dz642-b9j9dtk1yikp9h0",
+ created_at: "2023-01-23T22:50:50.788284000Z",
+ cumulative_cost: 0.00120553009559028,
+ cwd: "/var/spool/cwl",
+ description: "test decsription",
+ environment: {},
+ etag: "2es6px6q7uo0yqi2i291x8gd6",
+ expires_at: null,
+ filters: null,
+ href: "/container_requests/zzzzz-xvhdp-111111111111111",
+ kind: "arvados#containerRequest",
+ log_uuid: "zzzzz-4zz18-a1gxqy9o6zyrdy8",
+ modified_at: "2023-01-24T21:13:54.772612000Z",
+ modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
+ modified_by_user_uuid: "jutro-tpzed-vllbpebicy84rd5",
+ mounts: {
+ "/var/lib/cwl/cwl.input.json": {
+ capacity: 0,
+ commit: "",
+ content: {
+ input: {
+ basename: "logo.ai.no.whitespace.png",
+ class: "File",
+ location:
+ "keep:5d3238c4db721a92c98b0305a47b0485+75/logo.ai.no.whitespace.png",
+ },
+ reverse_sort: true,
+ },
+ device_type: "",
+ exclude_from_output: false,
+ git_url: "",
+ kind: "json",
+ path: "",
+ portable_data_hash: "",
+ repository_name: "",
+ uuid: "",
+ writable: false,
+ },
+ "/var/lib/cwl/workflow.json": {
+ capacity: 0,
+ commit: "",
+ content: {
+ $graph: [
+ {
+ class: "Workflow",
+ doc: "Reverse the lines in a document, then sort those lines.",
+ id: "#main",
+ inputs: [
+ {
+ default: null,
+ doc: "The input file to be processed.",
+ id: "#main/input",
+ type: "File",
+ },
+ {
+ default: true,
+ doc: "If true, reverse (decending) sort",
+ id: "#main/reverse_sort",
+ type: "boolean",
+ },
+ ],
+ outputs: [
+ {
+ doc: "The output with the lines reversed and sorted.",
+ id: "#main/output",
+ outputSource: "#main/sorted/output",
+ type: "File",
+ },
+ ],
+ steps: [
+ {
+ id: "#main/rev",
+ in: [{ id: "#main/rev/input", source: "#main/input" }],
+ out: ["#main/rev/output"],
+ run: "#revtool.cwl",
+ },
+ {
+ id: "#main/sorted",
+ in: [
+ { id: "#main/sorted/input", source: "#main/rev/output" },
+ {
+ id: "#main/sorted/reverse",
+ source: "#main/reverse_sort",
+ },
+ ],
+ out: ["#main/sorted/output"],
+ run: "#sorttool.cwl",
+ },
+ ],
+ },
+ {
+ baseCommand: "rev",
+ class: "CommandLineTool",
+ doc: "Reverse each line using the `rev` command",
+ hints: [{ class: "ResourceRequirement", ramMin: 8 }],
+ id: "#revtool.cwl",
+ inputs: [
+ { id: "#revtool.cwl/input", inputBinding: {}, type: "File" },
+ ],
+ outputs: [
+ {
+ id: "#revtool.cwl/output",
+ outputBinding: { glob: "output.txt" },
+ type: "File",
+ },
+ ],
+ stdout: "output.txt",
+ },
+ {
+ baseCommand: "sort",
+ class: "CommandLineTool",
+ doc: "Sort lines using the `sort` command",
+ hints: [{ class: "ResourceRequirement", ramMin: 8 }],
+ id: "#sorttool.cwl",
+ inputs: [
+ {
+ id: "#sorttool.cwl/reverse",
+ inputBinding: { position: 1, prefix: "-r" },
+ type: "boolean",
+ },
+ {
+ id: "#sorttool.cwl/input",
+ inputBinding: { position: 2 },
+ type: "File",
+ },
+ ],
+ outputs: [
+ {
+ id: "#sorttool.cwl/output",
+ outputBinding: { glob: "output.txt" },
+ type: "File",
+ },
+ ],
+ stdout: "output.txt",
+ },
+ ],
+ cwlVersion: "v1.0",
+ },
+ device_type: "",
+ exclude_from_output: false,
+ git_url: "",
+ kind: "json",
+ path: "",
+ portable_data_hash: "",
+ repository_name: "",
+ uuid: "",
+ writable: false,
+ },
+ "/var/spool/cwl": {
+ capacity: 0,
+ commit: "",
+ content: null,
+ device_type: "",
+ exclude_from_output: false,
+ git_url: "",
+ kind: "collection",
+ path: "",
+ portable_data_hash: "",
+ repository_name: "",
+ uuid: "",
+ writable: true,
+ },
+ stdout: {
+ capacity: 0,
+ commit: "",
+ content: null,
+ device_type: "",
+ exclude_from_output: false,
+ git_url: "",
+ kind: "file",
+ path: "/var/spool/cwl/cwl.output.json",
+ portable_data_hash: "",
+ repository_name: "",
+ uuid: "",
+ writable: false,
+ },
+ },
+ name: "Copy of: Copy of: Copy of: revsort.cwl",
+ output_name: "Output from revsort.cwl",
+ output_path: "/var/spool/cwl",
+ output_properties: { key: "val" },
+ output_storage_classes: ["default"],
+ output_ttl: 999999,
+ output_uuid: "zzzzz-4zz18-wolwlyfxmlhmgd4",
+ owner_uuid: "zzzzz-j7d0g-yr18k784zplfeza",
+ priority: 500,
+ properties: {
+ template_uuid: "zzzzz-7fd4e-7xsza0vgfe785cy",
+ workflowName: "revsort.cwl",
+ },
+ requesting_container_uuid: null,
+ runtime_constraints: {
+ API: true,
+ cuda: { device_count: 0, driver_version: "", hardware_capability: "" },
+ keep_cache_disk: 0,
+ keep_cache_ram: 0,
+ ram: 1342177280,
+ vcpus: 1,
+ },
+ runtime_token: "",
+ scheduling_parameters: {
+ max_run_time: 0,
+ partitions: [],
+ preemptible: false,
+ },
+ state: "Final",
+ use_existing: false,
+ uuid: "zzzzz-xvhdp-111111111111111",
+ };
+
+ let expectedContainerRequest = {
+ command: [
+ "arvados-cwl-runner",
+ "--api=containers",
+ "--local",
+ "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
+ "/var/lib/cwl/workflow.json#main",
+ "/var/lib/cwl/cwl.input.json",
+ ],
+ container_count_max: 10,
+ container_image: "arvados/jobs",
+ cwd: "/var/spool/cwl",
+ description: "test decsription",
+ environment: {},
+ kind: "arvados#containerRequest",
+ mounts: {
+ "/var/lib/cwl/cwl.input.json": {
+ capacity: 0,
+ commit: "",
+ content: {
+ input: {
+ basename: "logo.ai.no.whitespace.png",
+ class: "File",
+ location:
+ "keep:5d3238c4db721a92c98b0305a47b0485+75/logo.ai.no.whitespace.png",
+ },
+ reverse_sort: true,
+ },
+ device_type: "",
+ exclude_from_output: false,
+ git_url: "",
+ kind: "json",
+ path: "",
+ portable_data_hash: "",
+ repository_name: "",
+ uuid: "",
+ writable: false,
+ },
+ "/var/lib/cwl/workflow.json": {
+ capacity: 0,
+ commit: "",
+ content: {
+ $graph: [
+ {
+ class: "Workflow",
+ doc: "Reverse the lines in a document, then sort those lines.",
+ id: "#main",
+ inputs: [
+ {
+ default: null,
+ doc: "The input file to be processed.",
+ id: "#main/input",
+ type: "File",
+ },
+ {
+ default: true,
+ doc: "If true, reverse (decending) sort",
+ id: "#main/reverse_sort",
+ type: "boolean",
+ },
+ ],
+ outputs: [
+ {
+ doc: "The output with the lines reversed and sorted.",
+ id: "#main/output",
+ outputSource: "#main/sorted/output",
+ type: "File",
+ },
+ ],
+ steps: [
+ {
+ id: "#main/rev",
+ in: [{ id: "#main/rev/input", source: "#main/input" }],
+ out: ["#main/rev/output"],
+ run: "#revtool.cwl",
+ },
+ {
+ id: "#main/sorted",
+ in: [
+ {
+ id: "#main/sorted/input",
+ source: "#main/rev/output",
+ },
+ {
+ id: "#main/sorted/reverse",
+ source: "#main/reverse_sort",
+ },
+ ],
+ out: ["#main/sorted/output"],
+ run: "#sorttool.cwl",
+ },
+ ],
+ },
+ {
+ baseCommand: "rev",
+ class: "CommandLineTool",
+ doc: "Reverse each line using the `rev` command",
+ hints: [{ class: "ResourceRequirement", ramMin: 8 }],
+ id: "#revtool.cwl",
+ inputs: [
+ {
+ id: "#revtool.cwl/input",
+ inputBinding: {},
+ type: "File",
+ },
+ ],
+ outputs: [
+ {
+ id: "#revtool.cwl/output",
+ outputBinding: { glob: "output.txt" },
+ type: "File",
+ },
+ ],
+ stdout: "output.txt",
+ },
+ {
+ baseCommand: "sort",
+ class: "CommandLineTool",
+ doc: "Sort lines using the `sort` command",
+ hints: [{ class: "ResourceRequirement", ramMin: 8 }],
+ id: "#sorttool.cwl",
+ inputs: [
+ {
+ id: "#sorttool.cwl/reverse",
+ inputBinding: { position: 1, prefix: "-r" },
+ type: "boolean",
+ },
+ {
+ id: "#sorttool.cwl/input",
+ inputBinding: { position: 2 },
+ type: "File",
+ },
+ ],
+ outputs: [
+ {
+ id: "#sorttool.cwl/output",
+ outputBinding: { glob: "output.txt" },
+ type: "File",
+ },
+ ],
+ stdout: "output.txt",
+ },
+ ],
+ cwlVersion: "v1.0",
+ },
+ device_type: "",
+ exclude_from_output: false,
+ git_url: "",
+ kind: "json",
+ path: "",
+ portable_data_hash: "",
+ repository_name: "",
+ uuid: "",
+ writable: false,
+ },
+ "/var/spool/cwl": {
+ capacity: 0,
+ commit: "",
+ content: null,
+ device_type: "",
+ exclude_from_output: false,
+ git_url: "",
+ kind: "collection",
+ path: "",
+ portable_data_hash: "",
+ repository_name: "",
+ uuid: "",
+ writable: true,
+ },
+ stdout: {
+ capacity: 0,
+ commit: "",
+ content: null,
+ device_type: "",
+ exclude_from_output: false,
+ git_url: "",
+ kind: "file",
+ path: "/var/spool/cwl/cwl.output.json",
+ portable_data_hash: "",
+ repository_name: "",
+ uuid: "",
+ writable: false,
+ },
+ },
+ name: "newname.cwl",
+ output_name: "Output from revsort.cwl",
+ output_path: "/var/spool/cwl",
+ output_properties: { key: "val" },
+ output_storage_classes: ["default"],
+ output_ttl: 999999,
+ owner_uuid: "zzzzz-j7d0g-000000000000000",
+ priority: 500,
+ properties: {
+ template_uuid: "zzzzz-7fd4e-7xsza0vgfe785cy",
+ workflowName: "revsort.cwl",
+ },
+ runtime_constraints: {
+ API: true,
+ cuda: {
+ device_count: 0,
+ driver_version: "",
+ hardware_capability: "",
+ },
+ keep_cache_disk: 0,
+ keep_cache_ram: 0,
+ ram: 1342177280,
+ vcpus: 1,
+ },
+ scheduling_parameters: {
+ max_run_time: 0,
+ partitions: [],
+ preemptible: false,
+ },
+ state: "Uncommitted",
+ use_existing: false,
+ };
+
+ beforeEach(() => {
+ dispatch = jest.fn();
+ services = {
+ containerRequestService: {
+ get: jest.fn().mockImplementation(async () => (CommonService.mapResponseKeys({data: sampleFailedProcess}))),
+ create: jest.fn().mockImplementation(async (data) => (CommonService.mapKeys(snakeCase)(data))),
+ },
+ };
+ getState = () => ({
+ auth: {},
+ });
+ });
+
+ it("should request the failed process and return a copy with the proper fields", async () => {
+ // when
+ const newprocess = await copyProcess({
+ name: "newname.cwl",
+ uuid: "zzzzz-xvhdp-111111111111111",
+ ownerUuid: "zzzzz-j7d0g-000000000000000",
+ })(dispatch, getState, services);
+
+ // then
+ expect(services.containerRequestService.get).toHaveBeenCalledWith("zzzzz-xvhdp-111111111111111");
+ expect(newprocess).toEqual(expectedContainerRequest);
+
+ });
+});
import { getProcess } from 'store/processes/process';
import {snackbarActions, SnackbarKind} from 'store/snackbar/snackbar-actions';
import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { ContainerRequestState } from "models/container-request";
export const PROCESS_COPY_FORM_NAME = 'processCopyFormName';
dispatch(startSubmit(PROCESS_COPY_FORM_NAME));
try {
const process = await services.containerRequestService.get(resource.uuid);
- const { kind, containerImage, outputPath, outputName, containerCountMax, command, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters } = process;
- await services.containerRequestService.create({ command, containerImage, outputPath, ownerUuid: resource.ownerUuid, name: resource.name, kind, outputName, containerCountMax, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters });
+ const {
+ command,
+ containerCountMax,
+ containerImage,
+ cwd,
+ description,
+ environment,
+ kind,
+ mounts,
+ outputName,
+ outputPath,
+ outputProperties,
+ outputStorageClasses,
+ outputTtl,
+ properties,
+ runtimeConstraints,
+ schedulingParameters,
+ useExisting,
+ } = process;
+ const newProcess = await services.containerRequestService.create({
+ command,
+ containerCountMax,
+ containerImage,
+ cwd,
+ description,
+ environment,
+ kind,
+ mounts,
+ name: resource.name,
+ outputName,
+ outputPath,
+ outputProperties,
+ outputStorageClasses,
+ outputTtl,
+ ownerUuid: resource.ownerUuid,
+ priority: 500,
+ properties,
+ runtimeConstraints,
+ schedulingParameters,
+ state: ContainerRequestState.UNCOMMITTED,
+ useExisting,
+ });
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
- return process;
+ return newProcess;
} catch (e) {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
throw new Error('Could not copy the process.');
}
- };
\ No newline at end of file
+ };
export const getProcessStatus = ({ containerRequest, container }: Process): ProcessStatus => {
switch (true) {
+ case containerRequest.containerUuid && !container:
+ return ProcessStatus.UNKNOWN;
+
case containerRequest.state === ContainerRequestState.FINAL &&
container?.state !== ContainerState.COMPLETE:
// Request was finalized before its container started (or the
import { ProjectResource } from "models/project";
import { UserResource } from "models/user";
import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
+import { ContainerResource } from "models/container";
+import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
export const loadProcess = (containerRequestUuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process> => {
- const containerRequest = await services.containerRequestService.get(containerRequestUuid);
- dispatch<any>(updateResources([containerRequest]));
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process | undefined> => {
+ let containerRequest: ContainerRequestResource | undefined = undefined;
+ try {
+ containerRequest = await services.containerRequestService.get(containerRequestUuid);
+ dispatch<any>(updateResources([containerRequest]));
+ } catch {
+ return undefined;
+ }
if (containerRequest.outputUuid) {
- const collection = await services.collectionService.get(containerRequest.outputUuid);
- dispatch<any>(updateResources([collection]));
+ try {
+ const collection = await services.collectionService.get(containerRequest.outputUuid, false);
+ dispatch<any>(updateResources([collection]));
+ } catch {}
}
if (containerRequest.containerUuid) {
- const container = await services.containerService.get(containerRequest.containerUuid);
- dispatch<any>(updateResources([container]));
- if (container.runtimeUserUuid) {
- const runtimeUser = await services.userService.get(container.runtimeUserUuid);
- dispatch<any>(updateResources([runtimeUser]));
- }
+ let container: ContainerResource | undefined = undefined;
+ try {
+ container = await services.containerService.get(containerRequest.containerUuid, false);
+ dispatch<any>(updateResources([container]));
+ } catch {}
+
+ try{
+ if (container && container.runtimeUserUuid) {
+ const runtimeUser = await services.userService.get(container.runtimeUserUuid, false);
+ dispatch<any>(updateResources([runtimeUser]));
+ }
+ } catch {}
+
return { containerRequest, container };
}
return { containerRequest };
}
};
+export const startWorkflow = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const process = await services.containerRequestService.update(uuid, { state: ContainerRequestState.COMMITTED });
+ if (process) {
+ dispatch<any>(updateResources([process]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process started', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+ } else {
+ dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
+ }
+ } catch (e) {
+ dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
+ }
+ };
+
export const reRunProcess = (processUuid: string, workflowUuid: string) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const process = getResource<any>(processUuid)(getState().resources);
getAdvancedDataFromQuery
} from 'store/search-bar/search-bar-actions';
import { getSortColumn } from "store/data-explorer/data-explorer-reducer";
-import { joinFilters } from 'services/api/filter-builder';
+import { FilterBuilder, joinFilters } from 'services/api/filter-builder';
import { DataColumns } from 'components/data-table/data-table';
import { serializeResourceTypeFilters } from 'store//resource-type-filters/resource-type-filters';
import { ProjectPanelColumnNames } from 'views/project-panel/project-panel';
-import { Resource } from 'models/resource';
+import { Resource, ResourceKind } from 'models/resource';
+import { ContainerRequestResource } from 'models/container-request';
export class SearchResultsMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
.then((response) => {
api.dispatch(updateResources(response.items));
api.dispatch(appendItems(response));
+ // Request all containers for process status to be available
+ const containerRequests = response.items.filter((item) => item.kind === ResourceKind.CONTAINER_REQUEST) as ContainerRequestResource[];
+ const containerUuids = containerRequests.map(container => container.containerUuid).filter(uuid => uuid !== null) as string[];
+ containerUuids.length && this.services.containerService
+ .list({
+ filters: new FilterBuilder()
+ .addIn('uuid', containerUuids)
+ .getFilters()
+ }, false)
+ .then((containers) => {
+ api.dispatch(updateResources(containers.items));
+ });
}).catch(() => {
api.dispatch(couldNotFetchSearchResults(session.clusterId));
});
import { pluginConfig } from 'plugins';
import { MiddlewareListReducer } from 'common/plugintypes';
import { sidePanelReducer } from './side-panel/side-panel-reducer'
+import { bannerReducer } from './banner/banner-reducer';
declare global {
interface Window {
const createRootReducer = (services: ServiceRepository) => combineReducers({
auth: authReducer(services),
+ banner: bannerReducer,
collectionPanel: collectionPanelReducer,
collectionPanelFiles: collectionPanelFilesReducer,
contextMenu: contextMenuReducer,
try {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const parentContainerRequest = await this.services.containerRequestService.get(parentContainerRequestUuid);
- const containerRequests = await this.services.containerRequestService.list(
- {
- ...getParams(dataExplorer, parentContainerRequest) ,
- select: containerRequestFieldsNoMounts
- });
-
+ if (parentContainerRequest.containerUuid) {
+ const containerRequests = await this.services.containerRequestService.list(
+ {
+ ...getParams(dataExplorer, parentContainerRequest) ,
+ select: containerRequestFieldsNoMounts
+ });
+ api.dispatch(updateResources(containerRequests.items));
+ await api.dispatch<any>(loadMissingProcessesInformation(containerRequests.items));
+ // Populate the actual user view
+ api.dispatch(setItems(containerRequests));
+ }
api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
- api.dispatch(updateResources(containerRequests.items));
- await api.dispatch<any>(loadMissingProcessesInformation(containerRequests.items));
- // Populate the actual user view
- api.dispatch(setItems(containerRequests));
} catch {
api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch(couldNotFetchSubprocesses());
handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState) => {
dispatch<any>(loadProcessPanel(uuid));
const process = await dispatch<any>(processesActions.loadProcess(uuid));
- await dispatch<any>(
- activateSidePanelTreeItem(process.containerRequest.ownerUuid)
- );
- dispatch<any>(setProcessBreadcrumbs(uuid));
- dispatch<any>(loadDetailsPanel(uuid));
+ if (process) {
+ await dispatch<any>(
+ activateSidePanelTreeItem(process.containerRequest.ownerUuid)
+ );
+ dispatch<any>(setProcessBreadcrumbs(uuid));
+ dispatch<any>(loadDetailsPanel(uuid));
+ }
});
export const updateProcess =
kind: SnackbarKind.SUCCESS,
})
);
+ dispatch<any>(navigateTo(process.uuid));
} catch (e) {
dispatch(
snackbarActions.OPEN_SNACKBAR({
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { configure, shallow, mount } from "enzyme";
+import { BannerComponent } from './banner';
+import { Button } from "@material-ui/core";
+import Adapter from "enzyme-adapter-react-16";
+import servicesProvider from '../../common/service-provider';
+
+configure({ adapter: new Adapter() });
+
+jest.mock('../../common/service-provider', () => ({
+ getServices: jest.fn(),
+}));
+
+describe('<BannerComponent />', () => {
+
+ let props;
+
+ beforeEach(() => {
+ props = {
+ isOpen: false,
+ bannerUUID: undefined,
+ keepWebInlineServiceUrl: '',
+ openBanner: jest.fn(),
+ closeBanner: jest.fn(),
+ classes: {} as any,
+ }
+ });
+
+ it('renders without crashing', () => {
+ // when
+ const banner = shallow(<BannerComponent {...props} />);
+
+ // then
+ expect(banner.find(Button)).toHaveLength(1);
+ });
+
+ it('calls collectionService', () => {
+ // given
+ props.isOpen = true;
+ props.bannerUUID = '123';
+ const mocks = {
+ collectionService: {
+ files: jest.fn(() => ({ then: (callback) => callback([{ name: 'banner.html' }]) })),
+ getFileContents: jest.fn(() => ({ then: (callback) => callback('<h1>Test</h1>') }))
+ }
+ };
+ (servicesProvider.getServices as any).mockImplementation(() => mocks);
+
+ // when
+ const banner = mount(<BannerComponent {...props} />);
+
+ // then
+ expect(servicesProvider.getServices).toHaveBeenCalled();
+ expect(mocks.collectionService.files).toHaveBeenCalled();
+ expect(mocks.collectionService.getFileContents).toHaveBeenCalled();
+ expect(banner.html()).toContain('<h1>Test</h1>');
+ });
+});
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { useState, useCallback, useEffect } from 'react';
+import { Dialog, DialogContent, DialogActions, Button, StyleRulesCallback, withStyles, WithStyles } from "@material-ui/core";
+import { connect } from "react-redux";
+import { RootState } from "store/store";
+import bannerActions from "store/banner/banner-action";
+import { ArvadosTheme } from 'common/custom-theme';
+import servicesProvider from 'common/service-provider';
+import { Dispatch } from 'redux';
+
+type CssRules = 'dialogContent' | 'dialogContentIframe';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ dialogContent: {
+ minWidth: '550px',
+ minHeight: '500px',
+ display: 'block'
+ },
+ dialogContentIframe: {
+ minWidth: '550px',
+ minHeight: '500px'
+ }
+});
+
+interface BannerProps {
+ isOpen: boolean;
+ bannerUUID?: string;
+ keepWebInlineServiceUrl: string;
+};
+
+type BannerComponentProps = BannerProps & WithStyles<CssRules> & {
+ openBanner: Function,
+ closeBanner: Function,
+};
+
+const mapStateToProps = (state: RootState): BannerProps => ({
+ isOpen: state.banner.isOpen,
+ bannerUUID: state.auth.config.clusterConfig.Workbench.BannerUUID,
+ keepWebInlineServiceUrl: state.auth.config.keepWebInlineServiceUrl,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ openBanner: () => dispatch<any>(bannerActions.openBanner()),
+ closeBanner: () => dispatch<any>(bannerActions.closeBanner()),
+});
+
+export const BANNER_LOCAL_STORAGE_KEY = 'bannerFileData';
+
+export const BannerComponent = (props: BannerComponentProps) => {
+ const {
+ isOpen,
+ openBanner,
+ closeBanner,
+ bannerUUID,
+ keepWebInlineServiceUrl
+ } = props;
+ const [bannerContents, setBannerContents] = useState(`<h1>Loading ...</h1>`)
+
+ const onConfirm = useCallback(() => {
+ closeBanner();
+ }, [closeBanner])
+
+ useEffect(() => {
+ if (!!bannerUUID && bannerUUID !== "") {
+ servicesProvider.getServices().collectionService.files(bannerUUID)
+ .then(results => {
+ const bannerFileData = results.find(({name}) => name === 'banner.html');
+ const result = localStorage.getItem(BANNER_LOCAL_STORAGE_KEY);
+
+ if (result && result === JSON.stringify(bannerFileData) && !isOpen) {
+ return;
+ }
+
+ if (bannerFileData) {
+ servicesProvider.getServices()
+ .collectionService.getFileContents(bannerFileData)
+ .then(data => {
+ setBannerContents(data);
+ openBanner();
+ localStorage.setItem(BANNER_LOCAL_STORAGE_KEY, JSON.stringify(bannerFileData));
+ });
+ }
+ });
+ }
+ }, [bannerUUID, keepWebInlineServiceUrl, openBanner, isOpen]);
+
+ return (
+ <Dialog open={isOpen}>
+ <div data-cy='confirmation-dialog'>
+ <DialogContent className={props.classes.dialogContent}>
+ <div dangerouslySetInnerHTML={{ __html: bannerContents }}></div>
+ </DialogContent>
+ <DialogActions style={{ margin: '0px 24px 24px' }}>
+ <Button
+ data-cy='confirmation-dialog-ok-btn'
+ variant='contained'
+ color='primary'
+ type='submit'
+ onClick={onConfirm}>
+ Close
+ </Button>
+ </DialogActions>
+ </div>
+ </Dialog>
+ );
+}
+
+export const Banner = withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(BannerComponent));
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "store/favorites/favorites-actions";
import {
- RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon,
+ RenameIcon, ShareIcon, MoveToIcon, DetailsIcon,
RemoveIcon, ReRunProcessIcon, OutputIcon,
AdvancedIcon,
OpenIcon
import { openProcessUpdateDialog } from "store/processes/process-update-actions";
import { openCopyProcessDialog } from 'store/processes/process-copy-actions';
import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
-import { openRemoveProcessDialog, reRunProcess } from "store/processes/processes-actions";
+import { openRemoveProcessDialog } from "store/processes/processes-actions";
import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
-import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
import { navigateToOutput } from "store/process-panel/process-panel-actions";
import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
dispatch<any>(openInNewTabAction(resource));
}
},
- {
- icon: CopyIcon,
- name: "Copy to project",
- execute: (dispatch, resource) => {
- dispatch<any>(openCopyProcessDialog(resource));
- }
- },
{
icon: ReRunProcessIcon,
- name: "Re-run process",
+ name: "Copy and re-run process",
execute: (dispatch, resource) => {
- if(resource.workflowUuid) {
- dispatch<any>(reRunProcess(resource.uuid, resource.workflowUuid));
- } else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: `You can't re-run this process`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
- }
+ dispatch<any>(openCopyProcessDialog(resource));
}
},
{
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import 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/projects-tree-picker/tree-picker-field';
+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 ProcessRerunFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
+
+export const DialogProcessRerun = (props: ProcessRerunFormDialogProps & PickerIdProp) =>
+ <FormDialog
+ dialogTitle='Choose location for re-run'
+ formFields={CopyDialogFields(props.pickerId)}
+ submitLabel='Copy'
+ {...props}
+ />;
+
+const CopyDialogFields = memoize((pickerId: string) =>
+ () =>
+ <>
+ <Field
+ name='name'
+ component={TextField as any}
+ validate={COPY_NAME_VALIDATION}
+ label="Enter a new name for the copy" />
+ <Field
+ name="ownerUuid"
+ component={ProjectTreePickerField}
+ validate={COPY_FILE_VALIDATION}
+ pickerId={pickerId}/>
+ </>);
import { withDialog } from "store/dialog/with-dialog";
import { reduxForm } from 'redux-form';
import { PROCESS_COPY_FORM_NAME } from 'store/processes/process-copy-actions';
-import { DialogCopy } from "views-components/dialog-copy/dialog-copy";
+import { DialogProcessRerun } from "views-components/dialog-copy/dialog-process-rerun";
import { copyProcess } from 'store/workbench/workbench-actions';
import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
import { pickerId } from "store/tree-picker/picker-id";
}
}),
pickerId(PROCESS_COPY_FORM_NAME),
-)(DialogCopy);
\ No newline at end of file
+)(DialogProcessRerun);
// SPDX-License-Identifier: AGPL-3.0
import React from "react";
-import { Badge, MenuItem } from '@material-ui/core';
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { Badge, MenuItem } from "@material-ui/core";
import { DropdownMenu } from "components/dropdown-menu/dropdown-menu";
-import { NotificationIcon } from 'components/icon/icon';
-
-export const NotificationsMenu =
- () =>
- <DropdownMenu
- icon={
- <Badge
- badgeContent={0}
- color="primary">
- <NotificationIcon />
- </Badge>}
- id="account-menu"
- title="Notifications">
- <MenuItem>You are up to date</MenuItem>
- </DropdownMenu>;
+import { NotificationIcon } from "components/icon/icon";
+import bannerActions from "store/banner/banner-action";
+import { BANNER_LOCAL_STORAGE_KEY } from "views-components/baner/banner";
+import { RootState } from "store/store";
+const mapStateToProps = (state: RootState): NotificationsMenuProps => ({
+ isOpen: state.banner.isOpen,
+ bannerUUID: state.auth.config.clusterConfig.Workbench.BannerUUID,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ openBanner: () => dispatch<any>(bannerActions.openBanner()),
+});
+
+type NotificationsMenuProps = {
+ isOpen: boolean;
+ bannerUUID?: string;
+}
+
+type NotificationsMenuComponentProps = NotificationsMenuProps & {
+ openBanner: any;
+}
+
+export const NotificationsMenuComponent = (props: NotificationsMenuComponentProps) => {
+ const { isOpen, openBanner } = props;
+ const result = localStorage.getItem(BANNER_LOCAL_STORAGE_KEY);
+ const menuItems: any[] = [];
+
+ if (!isOpen && result) {
+ menuItems.push(<MenuItem><span onClick={openBanner}>Restore Banner</span></MenuItem>);
+ }
+
+ if (menuItems.length === 0) {
+ menuItems.push(<MenuItem>You are up to date</MenuItem>);
+ }
+
+ return (<DropdownMenu
+ icon={
+ <Badge
+ badgeContent={0}
+ color="primary">
+ <NotificationIcon />
+ </Badge>}
+ id="account-menu"
+ title="Notifications">
+ {
+ menuItems.map(item => item)
+ }
+ </DropdownMenu>);
+}
+
+export const NotificationsMenu = connect(mapStateToProps, mapDispatchToProps)(NotificationsMenuComponent);
export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
<div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
- <div style={{ flexBasis: '200px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+ <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
<ProjectsTreePicker
pickerId={props.pickerId}
toggleItemActive={handleChange(props)}
export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
<div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
- <div style={{ flexBasis: '200px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+ <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
<ProjectsTreePicker
pickerId={props.pickerId}
toggleItemActive={handleChange(props)}
);
// then
- expect(wrapper.text()).toContain("davs://bobby@download.example.com/by_id/zzzzz-4zz18-b1f8tbldjrm8885");
+ expect(wrapper.text()).toContain("davs://bobby@download.example.com/c=zzzzz-4zz18-b1f8tbldjrm8885");
});
it('render win/mac tab', () => {
);
// then
- expect(wrapper.text()).toContain("https://download.example.com/by_id/zzzzz-4zz18-b1f8tbldjrm8885");
+ expect(wrapper.text()).toContain("https://download.example.com/c=zzzzz-4zz18-b1f8tbldjrm8885");
});
it('render s3 tab with federated token', () => {
<key>Port</key>
<string>${(cyberDavStr.split(':')[2] || '443').split('/')[0]}</string>
<key>Username</key>
- <string>${username}</string>${isValidIpAddress(collectionsUrl.replace('https://', ``).split(':')[0])?
+ <string>${username}</string>${isValidIpAddress(collectionsUrl.replace('https://', ``).split(':')[0]) ?
`
<key>Path</key>
<string>/c=${uuid}</string>` : ''}
} else {
winDav = new URL(props.data.downloadUrl);
cyberDav = new URL(props.data.downloadUrl);
- winDav.pathname = `/by_id/${props.data.uuid}`;
- cyberDav.pathname = `/by_id/${props.data.uuid}`;
+ winDav.pathname = `/c=${props.data.uuid}`;
+ cyberDav.pathname = `/c=${props.data.uuid}`;
}
cyberDav.username = props.data.username;
</DetailsAttribute>
<p>
- Note: This curl command downloads single files.
- Append the desired filename to the end of the URL.
+ Note: This curl command downloads single files.
+ Append the desired filename to the end of the URL.
</p>
</TabPanel>
color='primary'
onClick={props.closeDialog}>
Close
- </Button>
+ </Button>
</DialogActions>
</Dialog >;
CardContent,
Tooltip,
Typography,
+ Button,
} from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
-import { CloseIcon, MoreOptionsIcon, ProcessIcon } from 'components/icon/icon';
+import { CloseIcon, MoreOptionsIcon, ProcessIcon, StartIcon } from 'components/icon/icon';
import { Process } from 'store/processes/process';
import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
import { ProcessDetailsAttributes } from './process-details-attributes';
import { ProcessStatus } from 'views-components/data-explorer/renderers';
import { ContainerState } from 'models/container';
+import { ContainerRequestState } from 'models/container-request';
-type CssRules = 'card' | 'content' | 'title' | 'header' | 'cancelButton' | 'avatar' | 'iconHeader';
+type CssRules = 'card' | 'content' | 'title' | 'header' | 'cancelButton' | 'avatar' | 'iconHeader' | 'runButton';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
cursor: 'pointer'
}
},
+ runButton: {
+ backgroundColor: theme.customs.colors.green700,
+ '&:hover': {
+ backgroundColor: theme.customs.colors.green800,
+ },
+ padding: "0px 5px 0 0",
+ marginRight: "5px",
+ },
});
export interface ProcessDetailsCardDataProps {
process: Process;
cancelProcess: (uuid: string) => void;
+ startProcess: (uuid: string) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
}
type ProcessDetailsCardProps = ProcessDetailsCardDataProps & WithStyles<CssRules> & MPVPanelProps;
export const ProcessDetailsCard = withStyles(styles)(
- ({ cancelProcess, onContextMenu, classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => {
+ ({ cancelProcess, startProcess, onContextMenu, classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => {
return <Card className={classes.card}>
<CardHeader
className={classes.header}
</Tooltip>}
action={
<div>
+ {process.containerRequest.state === ContainerRequestState.UNCOMMITTED &&
+ <Button
+ variant="contained"
+ size="small"
+ color="primary"
+ className={classes.runButton}
+ onClick={() => startProcess(process.containerRequest.uuid)}>
+ <StartIcon />
+ Run Process
+ </Button>}
{process.container && process.container.state === ContainerState.RUNNING &&
<span className={classes.cancelButton} onClick={() => cancelProcess(process.containerRequest.uuid)}>Cancel</span>}
<ProcessStatus uuid={process.containerRequest.uuid} />
onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
onToggle: (status: string) => void;
cancelProcess: (uuid: string) => void;
+ startProcess: (uuid: string) => void;
onLogFilterChange: (filter: FilterOption) => void;
navigateToLog: (uuid: string) => void;
onCopyToClipboard: (uuid: string) => void;
process={process}
onContextMenu={event => props.onContextMenu(event, process)}
cancelProcess={props.cancelProcess}
+ startProcess={props.startProcess}
/>
</MPVPanelContent>
<MPVPanelContent forwardProps xs="auto" data-cy="process-cmd">
updateOutputParams,
loadNodeJson
} from 'store/process-panel/process-panel-actions';
-import { cancelRunningWorkflow } from 'store/processes/processes-actions';
+import { cancelRunningWorkflow, startWorkflow } from 'store/processes/processes-actions';
import { navigateToLogCollection, setProcessLogsPanelFilter } from 'store/process-logs-panel/process-logs-panel-actions';
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
dispatch<any>(toggleProcessPanelFilter(status));
},
cancelProcess: (uuid) => dispatch<any>(cancelRunningWorkflow(uuid)),
+ startProcess: (uuid) => dispatch<any>(startWorkflow(uuid)),
onLogFilterChange: (filter) => dispatch(setProcessLogsPanelFilter(filter.value)),
navigateToLog: (uuid) => dispatch<any>(navigateToLogCollection(uuid)),
loadInputs: (containerRequest) => dispatch<any>(loadInputs(containerRequest)),
import { createServices } from "services/services";
import 'jest-localstorage-mock';
+jest.mock('views-components/baner/banner', () => ({ Banner: () => 'Banner' }))
+
const history = createBrowserHistory();
it('renders without crashing', () => {
import { pluginConfig } from 'plugins';
import { ElementListReducer } from 'common/plugintypes';
import { COLLAPSE_ICON_SIZE } from 'views-components/side-panel-toggle/side-panel-toggle'
+import { Banner } from 'views-components/baner/banner';
type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
<VirtualMachineAttributesDialog />
<FedLogin />
<WebDavS3InfoDialog />
+ <Banner />
{React.createElement(React.Fragment, null, pluginConfig.dialogs)}
</Grid>}
);