Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>
cy.goToPath(`/collections/${this.testCollection.uuid}`);
['subdir', 'G%C3%BCnter\'s%20file', 'table%&?*2'].forEach((subdir) => {
- cy.get('[data-cy=collection-files-panel]')
+ cy.waitForDom().get('[data-cy=collection-files-panel]')
.contains('bar').rightclick();
cy.get('[data-cy=context-menu]')
.contains('Rename')
.contains(projName).and('contain', testProject.uuid);
// Double check that the collection is in the project
cy.goToPath(`/projects/${testProject.uuid}`);
- cy.get('[data-cy=project-panel]').should('contain', collName);
+ cy.waitForDom().get('[data-cy=project-panel]').should('contain', collName);
});
});
cy.getAll('@stdoutLogs', '@nodeInfoLogs', '@crunchRunLogs').then(function() {
cy.loginAs(activeUser);
cy.goToPath(`/processes/${containerRequest.uuid}`);
- // Should should all logs
- cy.get('[data-cy=process-logs-filter]').should('contain', 'All logs');
+ // Should show main logs by default
+ cy.get('[data-cy=process-logs-filter]').should('contain', 'Main logs');
+ cy.get('[data-cy=process-logs]')
+ .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
+ .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
+ .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
+ // Select 'All logs'
+ cy.get('[data-cy=process-logs-filter]').click();
+ cy.get('body').contains('li', 'All logs').click();
cy.get('[data-cy=process-logs]')
.should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
.and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
.should('contain', 'Free disk space is low')
.and('contain', 'No additional warning details available');
});
+
+
+ // Force container_count for testing
+ let containerCount = 2;
+ cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
+ req.reply((res) => {
+ res.body.container_count = containerCount;
+ });
+ });
+
+ cy.getAll('@containerRequest').then(function([containerRequest]) {
+ cy.goToPath(`/processes/${containerRequest.uuid}`);
+ cy.get('[data-cy=process-runtime-status-retry-warning]')
+ .should('contain', 'Process retried 1 time');
+ });
+
+ cy.getAll('@containerRequest').then(function([containerRequest]) {
+ containerCount = 3;
+ cy.goToPath(`/processes/${containerRequest.uuid}`);
+ cy.get('[data-cy=process-runtime-status-retry-warning]')
+ .should('contain', 'Process retried 2 times');
+ });
});
});
// Pink is not in the test vocab
{IDTAGCOLORS: ['IDVALCOLORS3', 'Pink', 'IDVALCOLORS1']});
});
+
+ // Open project edit via breadcrumbs
+ cy.get('[data-cy=breadcrumbs]').contains(projName).rightclick();
+ cy.get('[data-cy=context-menu]').contains('Edit').click();
+ cy.get('[data-cy=form-dialog]').within(() => {
+ cy.get('[data-cy=resource-properties-list]').within(() => {
+ cy.get('div[role=button]').contains('Color: Magenta');
+ cy.get('div[role=button]').contains('Color: Pink');
+ cy.get('div[role=button]').contains('Color: Yellow');
+ });
+ });
+ // Add another property
+ cy.get('[data-cy=resource-properties-form]').within(() => {
+ cy.get('[data-cy=property-field-key]').within(() => {
+ cy.get('input').type('Animal');
+ });
+ cy.get('[data-cy=property-field-value]').within(() => {
+ cy.get('input').type('Dog');
+ });
+ cy.root().submit();
+ });
+ cy.get('[data-cy=form-submit-btn]').click();
+ // Reopen edit via breadcrumbs and verify properties
+ cy.get('[data-cy=breadcrumbs]').contains(projName).rightclick();
+ cy.get('[data-cy=context-menu]').contains('Edit').click();
+ cy.get('[data-cy=form-dialog]').within(() => {
+ cy.get('[data-cy=resource-properties-list]').within(() => {
+ cy.get('div[role=button]').contains('Color: Magenta');
+ cy.get('div[role=button]').contains('Color: Pink');
+ cy.get('div[role=button]').contains('Color: Yellow');
+ cy.get('div[role=button]').contains('Animal: Dog');
+ });
+ });
});
it('creates new project on home project and then a subproject inside it', function() {
});
});
});
-});
\ No newline at end of file
+
+ it('shows search context menu', function() {
+ const colName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+ const federatedColName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+ const federatedColUuid = "xxxxx-4zz18-000000000000000";
+
+ // Intercept config to insert remote cluster
+ cy.intercept({method: 'GET', hostname: 'localhost', url: '**/arvados/v1/config?nocache=*'}, (req) => {
+ req.reply((res) => {
+ res.body.RemoteClusters = {
+ "*": res.body.RemoteClusters["*"],
+ "xxxxx": {
+ "ActivateUsers": true,
+ "Host": "xxxxx.fakecluster.tld",
+ "Insecure": false,
+ "Proxy": true,
+ "Scheme": ""
+ }
+ };
+ });
+ });
+
+ // Fake remote cluster config
+ cy.intercept(
+ {
+ method: "GET",
+ hostname: "xxxxx.fakecluster.tld",
+ url: "**/arvados/v1/config",
+ },
+ {
+ statusCode: 200,
+ body: {
+ API: {},
+ ClusterID: "xxxxx",
+ Collections: {},
+ Containers: {},
+ InstanceTypes: {},
+ Login: {},
+ Mail: { SupportEmailAddress: "arvados@example.com" },
+ RemoteClusters: {
+ "*": {
+ ActivateUsers: false,
+ Host: "",
+ Insecure: false,
+ Proxy: false,
+ Scheme: "https",
+ },
+ },
+ Services: {
+ Composer: { ExternalURL: "" },
+ Controller: { ExternalURL: "https://xxxxx.fakecluster.tld:34763/" },
+ DispatchCloud: { ExternalURL: "" },
+ DispatchLSF: { ExternalURL: "" },
+ DispatchSLURM: { ExternalURL: "" },
+ GitHTTP: { ExternalURL: "https://xxxxx.fakecluster.tld:39105/" },
+ GitSSH: { ExternalURL: "" },
+ Health: { ExternalURL: "https://xxxxx.fakecluster.tld:42915/" },
+ Keepbalance: { ExternalURL: "" },
+ Keepproxy: { ExternalURL: "https://xxxxx.fakecluster.tld:46773/" },
+ Keepstore: { ExternalURL: "" },
+ RailsAPI: { ExternalURL: "" },
+ WebDAV: { ExternalURL: "https://xxxxx.fakecluster.tld:36041/" },
+ WebDAVDownload: { ExternalURL: "https://xxxxx.fakecluster.tld:42957/" },
+ WebShell: { ExternalURL: "" },
+ Websocket: { ExternalURL: "wss://xxxxx.fakecluster.tld:37121/websocket" },
+ Workbench1: { ExternalURL: "https://wb1.xxxxx.fakecluster.tld/" },
+ Workbench2: { ExternalURL: "https://wb2.xxxxx.fakecluster.tld/" },
+ },
+ StorageClasses: {
+ default: { Default: true, Priority: 0 },
+ },
+ Users: {},
+ Volumes: {},
+ Workbench: {},
+ },
+ }
+ );
+
+ cy.createCollection(adminUser.token, {
+ name: colName,
+ owner_uuid: activeUser.user.uuid,
+ preserve_version: true,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ }).then(function(testCollection) {
+ cy.loginAs(activeUser);
+
+ // Intercept search results to add federated result
+ cy.intercept({method: 'GET', url: '**/arvados/v1/groups/contents?*'}, (req) => {
+ req.reply((res) => {
+ res.body.items = [
+ res.body.items[0],
+ {
+ ...res.body.items[0],
+ uuid: federatedColUuid,
+ portable_data_hash: "00000000000000000000000000000000+0",
+ name: federatedColName,
+ href: res.body.items[0].href.replace(testCollection.uuid, federatedColUuid),
+ }
+ ];
+ res.body.items_available += 1;
+ });
+ });
+
+ cy.doSearch(colName);
+
+ // Stub new window
+ cy.window().then(win => {
+ cy.stub(win, 'open').as('Open')
+ });
+
+ // Check copy to clipboard
+ cy.get('[data-cy=search-results]').contains(colName).rightclick();
+ cy.get('[data-cy=context-menu]').within((ctx) => {
+ // Check that there are 4 items in the menu
+ cy.get(ctx).children().should('have.length', 4);
+ cy.contains('Advanced');
+ cy.contains('Copy to clipboard');
+ cy.contains('Open in new tab');
+ cy.contains('View details');
+
+ cy.contains('Copy to clipboard').click();
+ cy.window().then((win) => (
+ win.navigator.clipboard.readText().then((text) => {
+ expect(text).to.match(new RegExp(`/collections/${testCollection.uuid}$`));
+ })
+ ));
+ });
+
+ // Check open in new tab
+ cy.get('[data-cy=search-results]').contains(colName).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Open in new tab').click();
+ cy.get('@Open').should('have.been.calledOnceWith', `${window.location.origin}/collections/${testCollection.uuid}`)
+ });
+
+ // Check federated result copy to clipboard
+ cy.get('[data-cy=search-results]').contains(federatedColName).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Copy to clipboard').click();
+ cy.window().then((win) => (
+ win.navigator.clipboard.readText().then((text) => {
+ expect(text).to.equal(`https://wb2.xxxxx.fakecluster.tld/collections/${federatedColUuid}`);
+ })
+ ));
+ });
+ // Check open in new tab
+ cy.get('[data-cy=search-results]').contains(federatedColName).rightclick();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.contains('Open in new tab').click();
+ cy.get('@Open').should('have.been.calledWith', `https://wb2.xxxxx.fakecluster.tld/collections/${federatedColUuid}`)
+ });
+
+ });
+ });
+});
"@types/react-virtualized-auto-sizer": "1.0.0",
"@types/react-window": "1.8.2",
"@types/redux-form": "7.4.12",
- "@types/shell-quote": "1.6.0",
+ "@types/shell-escape": "^0.2.0",
"axios": "^0.21.1",
"babel-core": "6.26.3",
"babel-runtime": "6.26.0",
"redux-thunk": "2.3.0",
"reselect": "4.0.0",
"set-value": "2.0.1",
- "shell-quote": "1.6.1",
+ "shell-escape": "^0.2.0",
"sinon": "7.3",
"tslint": "5.20.0",
"tslint-etc": "1.6.0",
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { StyleRulesCallback, WithStyles, Typography, withStyles } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, Typography, withStyles, Link } from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
import classNames from 'classnames';
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from 'store/store';
+import { FederationConfig, getNavUrl } from 'routes/routes';
+import { Dispatch } from 'redux';
+import { navigationNotAvailable } from 'store/navigation/navigation-action';
type CssRules = 'root' | 'space';
lines: string[];
className?: string;
apiResponse?: boolean;
+ linked?: boolean;
+}
+
+interface CodeSnippetAuthProps {
+ auth: FederationConfig;
}
type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
-export const CodeSnippet = withStyles(styles)(
- ({ classes, lines, className, apiResponse }: CodeSnippetProps) =>
+const mapStateToProps = (state: RootState): CodeSnippetAuthProps => ({
+ auth: state.auth,
+});
+
+export const CodeSnippet = withStyles(styles)(connect(mapStateToProps)(
+ ({ classes, lines, linked, className, apiResponse, dispatch, auth }: CodeSnippetProps & CodeSnippetAuthProps & DispatchProp) =>
<Typography
component="div"
className={classNames(classes.root, className)}>
- {
- lines.map((line: string, index: number) => {
- return <Typography key={index} className={apiResponse ? classes.space : className} component="pre">{line}</Typography>;
- })
- }
+ <Typography className={apiResponse ? classes.space : className} component="pre">
+ {linked ?
+ lines.map((line, index) => <React.Fragment key={index}>{renderLinks(auth, dispatch)(line)}{`\n`}</React.Fragment>) :
+ lines.join('\n')
+ }
+ </Typography>
</Typography>
- );
\ No newline at end of file
+ ));
+
+const renderLinks = (auth: FederationConfig, dispatch: Dispatch) => (text: string): JSX.Element => {
+ // Matches UUIDs & PDHs
+ const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
+ const links = text.match(REGEX);
+ if (!links) {
+ return <>{text}</>;
+ }
+ return <>
+ {text.split(REGEX).map((part, index) =>
+ <React.Fragment key={index}>
+ {part}
+ {links[index] &&
+ <Link onClick={() => {
+ const url = getNavUrl(links[index], auth)
+ if (url) {
+ window.open(`${window.location.origin}${url}`, '_blank');
+ } else {
+ dispatch(navigationNotAvailable(links[index]));
+ }
+ }}
+ style={ {cursor: 'pointer'} }>
+ {links[index]}
+ </Link>}
+ </React.Fragment>
+ )}
+ </>;
+ };
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
import { CodeSnippet, CodeSnippetDataProps } from 'components/code-snippet/code-snippet';
import grey from '@material-ui/core/colors/grey';
+import { themeOptions } from 'common/custom-theme';
-const theme = createMuiTheme({
+const theme = createMuiTheme(Object.assign({}, themeOptions, {
overrides: {
MuiTypography: {
body1: {
fontFamily: 'monospace',
useNextVariants: true,
}
-});
+}));
-export const DefaultCodeSnippet = (props: CodeSnippetDataProps) =>
+export const DefaultCodeSnippet = (props: CodeSnippetDataProps) =>
<MuiThemeProvider theme={theme}>
<CodeSnippet {...props} />
- </MuiThemeProvider>;
\ No newline at end of file
+ </MuiThemeProvider>;
// Import FontAwesome icons
import { library } from '@fortawesome/fontawesome-svg-core';
import { faPencilAlt, faSlash, faUsers, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
+import { FormatAlignLeft } from '@material-ui/icons';
library.add(
faPencilAlt,
faSlash,
export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
export const CanManageIcon: IconType = (props) => <Computer {...props} />;
export const AddUserIcon: IconType = (props) => <PersonAdd {...props} />;
-export const WordWrapIcon: IconType = (props) => <WrapText {...props} />;
+export const WordWrapOnIcon: IconType = (props) => <WrapText {...props} />;
+export const WordWrapOffIcon: IconType = (props) => <FormatAlignLeft {...props} />;
export const TextIncreaseIcon: IconType = (props) => <TextIncrease {...props} />;
export const TextDecreaseIcon: IconType = (props) => <TextDecrease {...props} />;
export const DeactivateUserIcon: IconType = (props) => <NotInterested {...props} />;
import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
import { openNotFoundDialog } from './store/not-found-panel/not-found-panel-action';
import { storeRedirects } from './common/redirect-to';
+import { searchResultsActionSet } from 'views-components/context-menu/action-sets/search-results-action-set';
console.log(`Starting arvados [${getBuildInfo()}]`);
addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
addMenuActionSet(ContextMenuKind.PERMISSION_EDIT, permissionEditActionSet);
addMenuActionSet(ContextMenuKind.WORKFLOW, workflowActionSet);
+addMenuActionSet(ContextMenuKind.SEARCH_RESULTS, searchResultsActionSet);
storeRedirects();
STDOUT = 'stdout',
STDERR = 'stderr',
CONTAINER = 'container',
+ KEEPSTORE = 'keepstore',
}
export interface LogResource extends Resource, ResourceWithProperties {
kind: ResourceKind.LOG;
objectUuid: string;
eventAt: string;
- eventType: string;
+ eventType: LogEventType;
summary: string;
}
}
};
-export const getNavUrl = (uuid: string, config: FederationConfig) => {
+/**
+ * @returns A relative or federated url for the given uuid, with a token for federated WB1 urls
+ */
+export const getNavUrl = (uuid: string, config: FederationConfig, includeToken: boolean = true): string => {
const path = getResourceUrl(uuid) || "";
const cls = uuid.substring(0, 5);
if (cls === config.localCluster || extractUuidKind(uuid) === ResourceKind.USER || COLLECTION_PDH_REGEX.exec(uuid)) {
u = new URL(config.remoteHostsConfig[cls].workbench2Url);
} else {
u = new URL(config.remoteHostsConfig[cls].workbenchUrl);
- u.search = "api_token=" + config.sessions.filter((s) => s.clusterId === cls)[0].token;
+ if (includeToken) {
+ u.search = "api_token=" + config.sessions.filter((s) => s.clusterId === cls)[0].token;
+ }
}
u.pathname = path;
return u.toString();
}
}
+ public setTargetUrl(url: string) {
+ localStorage.setItem(TARGET_URL, url);
+ }
+
public removeTargetURL() {
localStorage.removeItem(TARGET_URL);
- sessionStorage.removeItem(TARGET_URL);
}
public getTargetURL() {
- return this.getStorage().getItem(TARGET_URL);
+ return localStorage.getItem(TARGET_URL);
}
public removeApiToken() {
const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
const homeClusterHost = remoteHosts[homeCluster];
const rd = new URL(window.location.href);
- this.getStorage().setItem(TARGET_URL, rd.pathname + rd.search);
+ this.setTargetUrl(rd.pathname + rd.search);
window.location.assign(`https://${homeClusterHost}/login?${(uuidPrefix !== homeCluster && homeCluster !== loginCluster) ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
}
try {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const processItems = await this.services.containerRequestService.list(
- { ...getParams(dataExplorer) });
+ {
+ ...getParams(dataExplorer),
+ // Omit mounts when viewing all process panel
+ select: containerRequestFieldsNoMounts,
+ });
api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch(resourcesActions.SET_RESOURCES(processItems.items));
}
}
+// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
+export const containerRequestFieldsNoMounts = [
+ "command",
+ "container_count_max",
+ "container_count",
+ "container_image",
+ "container_uuid",
+ "created_at",
+ "cwd",
+ "description",
+ "environment",
+ "etag",
+ "expires_at",
+ "filters",
+ "href",
+ "kind",
+ "log_uuid",
+ "modified_at",
+ "modified_by_client_uuid",
+ "modified_by_user_uuid",
+ "name",
+ "output_name",
+ "output_path",
+ "output_properties",
+ "output_storage_classes",
+ "output_ttl",
+ "output_uuid",
+ "owner_uuid",
+ "priority",
+ "properties",
+ "requesting_container_uuid",
+ "runtime_constraints",
+ "scheduling_parameters",
+ "state",
+ "use_existing",
+ "uuid",
+];
+
const getParams = ( dataExplorer: DataExplorer ) => ({
...dataExplorerToListParams(dataExplorer),
order: getOrder(dataExplorer),
import { getResource, getResourceWithEditableStatus } from '../resources/resources';
import { UserResource } from 'models/user';
import { isSidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions';
-import { extractUuidKind, ResourceKind, EditableResource } from 'models/resource';
+import { extractUuidKind, ResourceKind, EditableResource, Resource } from 'models/resource';
import { Process } from 'store/processes/process';
import { RepositoryResource } from 'models/repositories';
import { SshKeyResource } from 'models/ssh-key';
return;
}
};
+
+export const openSearchResultsContextMenu = (event: React.MouseEvent<HTMLElement>, uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const res = getResource<Resource>(uuid)(getState().resources);
+ if (res) {
+ dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: res.uuid,
+ ownerUuid: '',
+ kind: res.kind,
+ menuKind: ContextMenuKind.SEARCH_RESULTS,
+ }));
+ }
+ };
itemsAvailable: 0,
page: 0,
rowsPerPage: 50,
- rowsPerPageOptions: [50, 100, 200, 500],
+ rowsPerPageOptions: [10, 20, 50, 100, 200, 500],
searchValue: "",
requestState: DataTableRequestState.IDLE
};
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from 'store/breadcrumbs/breadcrumbs-actions';
-const navigationNotAvailable = (id: string) =>
+export const navigationNotAvailable = (id: string) =>
snackbarActions.OPEN_SNACKBAR({
message: `${id} not available`,
hideDuration: 3000,
// SPDX-License-Identifier: AGPL-3.0
import copy from 'copy-to-clipboard';
-import { ResourceKind } from 'models/resource';
-import { getClipboardUrl } from 'views-components/context-menu/actions/helpers';
+import { Dispatch } from 'redux';
+import { getNavUrl } from 'routes/routes';
+import { RootState } from 'store/store';
-const getUrl = (resource: any) => {
- let url: string | null = null;
- const { uuid, kind } = resource;
+export const openInNewTabAction = (resource: any) => (dispatch: Dispatch, getState: () => RootState) => {
+ const url = getNavUrl(resource.uuid, getState().auth);
- if (kind === ResourceKind.COLLECTION) {
- url = `/collections/${uuid}`;
- }
- if (kind === ResourceKind.PROJECT) {
- url = `/projects/${uuid}`;
- }
-
- return url;
-};
-
-export const openInNewTabAction = (resource: any) => () => {
- const url = getUrl(resource);
-
- if (url) {
+ if (url[0] === '/') {
window.open(`${window.location.origin}${url}`, '_blank');
+ } else if (url.length) {
+ window.open(url, '_blank');
}
};
-export const copyToClipboardAction = (resource: any) => () => {
- const url = getUrl(resource);
+export const copyToClipboardAction = (resource: any) => (dispatch: Dispatch, getState: () => RootState) => {
+ // Copy to clipboard omits token to avoid accidental sharing
+ const url = getNavUrl(resource.uuid, getState().auth, false);
if (url) {
- copy(getClipboardUrl(url, false));
+ copy(url);
}
-};
\ No newline at end of file
+};
async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => {
if (PROCESS_PANEL_LOG_EVENT_TYPES.indexOf(message.eventType) > -1) {
const uuid = getProcessLogsPanelCurrentUuid(getState().router);
- if (uuid) {
- const process = getProcess(uuid)(getState().resources);
- if (process) {
- const { containerRequest, container } = process;
- if (message.objectUuid === containerRequest.uuid
- || (container && message.objectUuid === container.uuid)) {
- dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
- logType: COMBINED_FILTER_TYPE,
- log: message.properties.text
- }));
- dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
- logType: message.eventType,
- log: message.properties.text
- }));
- }
+ if (!uuid) { return }
+ const process = getProcess(uuid)(getState().resources);
+ if (!process) { return }
+ const { containerRequest, container } = process;
+ if (message.objectUuid === containerRequest.uuid
+ || (container && message.objectUuid === container.uuid)) {
+ dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+ logType: ALL_FILTER_TYPE,
+ log: message.properties.text
+ }));
+ dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+ logType: message.eventType,
+ log: message.properties.text
+ }));
+ if (MAIN_EVENT_TYPES.indexOf(message.eventType) > -1) {
+ dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+ logType: MAIN_FILTER_TYPE,
+ log: message.properties.text
+ }));
}
}
}
const createInitialLogPanelState = (logResources: LogResource[]) => {
const allLogs = logsToLines(logResources);
+ const mainLogs = logsToLines(logResources.filter(
+ e => MAIN_EVENT_TYPES.indexOf(e.eventType) > -1
+ ));
const groupedLogResources = groupBy(logResources, log => log.eventType);
const groupedLogs = Object
.keys(groupedLogResources)
...grouped,
[key]: logsToLines(groupedLogResources[key])
}), {});
- const filters = [COMBINED_FILTER_TYPE, ...Object.keys(groupedLogs)];
- const logs = { [COMBINED_FILTER_TYPE]: allLogs, ...groupedLogs };
+ const filters = [MAIN_FILTER_TYPE, ALL_FILTER_TYPE, ...Object.keys(groupedLogs)];
+ const logs = {
+ [MAIN_FILTER_TYPE]: mainLogs,
+ [ALL_FILTER_TYPE]: allLogs,
+ ...groupedLogs
+ };
return { filters, logs };
};
const MAX_AMOUNT_OF_LOGS = 10000;
-const COMBINED_FILTER_TYPE = 'All logs';
+const ALL_FILTER_TYPE = 'All logs';
+
+const MAIN_FILTER_TYPE = 'Main logs';
+const MAIN_EVENT_TYPES = [
+ LogEventType.CRUNCH_RUN,
+ LogEventType.STDERR,
+ LogEventType.STDOUT,
+];
const PROCESS_PANEL_LOG_EVENT_TYPES = [
LogEventType.ARV_MOUNT,
LogEventType.STDERR,
LogEventType.STDOUT,
LogEventType.CONTAINER,
+ LogEventType.KEEPSTORE,
];
selectedFilter
}),
ADD_PROCESS_LOGS_PANEL_ITEM: ({ logType, log }) => {
+ const filters = state.filters.indexOf(logType) > -1
+ ? state.filters
+ : [...state.filters, logType];
const currentLogs = state.logs[logType] || [];
const logsOfType = [...currentLogs, log];
const logs = { ...state.logs, [logType]: logsOfType };
- return { ...state, logs };
+ return { ...state, logs, filters };
},
default: () => state,
});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { dialogActions } from 'store/dialog/dialog-actions';
-import { RootState } from 'store/store';
-import { Dispatch } from 'redux';
-import { getProcess } from 'store/processes/process';
-import { quote } from 'shell-quote';
-
-export const PROCESS_COMMAND_DIALOG_NAME = 'processCommandDialog';
-
-export interface ProcessCommandDialogData {
- command: string;
- processName: string;
-}
-
-export const openProcessCommandDialog = (processUuid: string) =>
- (dispatch: Dispatch<any>, getState: () => RootState) => {
- const process = getProcess(processUuid)(getState().resources);
- if (process) {
- const data: ProcessCommandDialogData = {
- command: quote(process.containerRequest.command),
- processName: process.containerRequest.name,
- };
- dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COMMAND_DIALOG_NAME, data }));
- }
- };
return { containerRequest };
};
-export const loadContainers = (filters: string) =>
+export const loadContainers = (filters: string, loadMounts: boolean = true) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { items } = await services.containerService.list({ filters });
+ let args: any = { filters };
+ if (!loadMounts) {
+ args.select = containerFieldsNoMounts;
+ }
+ const { items } = await services.containerService.list(args);
dispatch<any>(updateResources(items));
return items;
};
+// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
+const containerFieldsNoMounts = [
+ "auth_uuid",
+ "command",
+ "container_image",
+ "created_at",
+ "cwd",
+ "environment",
+ "etag",
+ "exit_code",
+ "finished_at",
+ "gateway_address",
+ "href",
+ "interactive_session_started",
+ "kind",
+ "lock_count",
+ "locked_by_uuid",
+ "log",
+ "modified_at",
+ "modified_by_client_uuid",
+ "modified_by_user_uuid",
+ "output_path",
+ "output_properties",
+ "output_storage_classes",
+ "output",
+ "owner_uuid",
+ "priority",
+ "progress",
+ "runtime_auth_scopes",
+ "runtime_constraints",
+ "runtime_status",
+ "runtime_user_uuid",
+ "scheduling_parameters",
+ "started_at",
+ "state",
+ "uuid",
+]
+
export const cancelRunningWorkflow = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
try {
}, []);
if (containerUuids.length > 0) {
await dispatch<any>(loadContainers(
- new FilterBuilder().addIn('uuid', containerUuids).getFilters()
+ new FilterBuilder().addIn('uuid', containerUuids).getFilters(),
+ false
));
}
};
import { GroupClass } from "models/group";
import { Participant } from "views-components/sharing-dialog/participant-select";
import { ProjectProperties } from "./project-create-actions";
+import { getResource } from "store/resources/resources";
+import { ProjectResource } from "models/project";
export interface ProjectUpdateFormDialogData {
uuid: string;
export const openProjectUpdateDialog = (resource: ProjectUpdateFormDialogData) =>
(dispatch: Dispatch, getState: () => RootState) => {
- dispatch(initialize(PROJECT_UPDATE_FORM_NAME, resource));
+ // Get complete project resource from store to handle consumers passing in partial resources
+ const project = getResource<ProjectResource>(resource.uuid)(getState().resources);
+ dispatch(initialize(PROJECT_UPDATE_FORM_NAME, project));
dispatch(dialogActions.OPEN_DIALOG({
id: PROJECT_UPDATE_FORM_NAME,
data: {
import { ContainerRequestResource } from 'models/container-request';
import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
import { loadMissingProcessesInformation } from '../project-panel/project-panel-middleware-service';
+import { containerRequestFieldsNoMounts } from 'store/all-processes-panel/all-processes-panel-middleware-service';
export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
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) });
+ {
+ ...getParams(dataExplorer, parentContainerRequest) ,
+ select: containerRequestFieldsNoMounts
+ });
api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch(updateResources(containerRequests.items));
import { toggleFavorite } from "store/favorites/favorites-actions";
import {
RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon,
- RemoveIcon, ReRunProcessIcon, InputIcon, OutputIcon, CommandIcon,
+ RemoveIcon, ReRunProcessIcon, OutputIcon,
AdvancedIcon
} from "components/icon/icon";
import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
import { openRemoveProcessDialog, reRunProcess } from "store/processes/processes-actions";
import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-import { openProcessInputDialog } from "store/processes/process-input-actions";
import { navigateToOutput } from "store/process-panel/process-panel-actions";
-import { openProcessCommandDialog } from "store/processes/process-command-actions";
import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
}
}
},
- {
- icon: InputIcon,
- name: "Inputs",
- execute: (dispatch, resource) => {
- dispatch<any>(openProcessInputDialog(resource.uuid));
- }
- },
{
icon: OutputIcon,
name: "Outputs",
}
}
},
- {
- icon: CommandIcon,
- name: "Command",
- execute: (dispatch, resource) => {
- dispatch<any>(openProcessCommandDialog(resource.uuid));
- }
- },
{
icon: DetailsIcon,
name: "View details",
});
}
},
-]];
\ No newline at end of file
+]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { DetailsIcon, AdvancedIcon, OpenIcon, Link } from 'components/icon/icon';
+import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
+import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
+import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+
+export const searchResultsActionSet: ContextMenuActionSet = [
+ [
+ {
+ icon: OpenIcon,
+ name: "Open in new tab",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openInNewTabAction(resource));
+ }
+ },
+ {
+ icon: Link,
+ name: "Copy to clipboard",
+ execute: (dispatch, resource) => {
+ dispatch<any>(copyToClipboardAction(resource));
+ }
+ },
+ {
+ icon: DetailsIcon,
+ name: "View details",
+ execute: dispatch => {
+ dispatch<any>(toggleDetailsPanel());
+ }
+ },
+ {
+ icon: AdvancedIcon,
+ name: "Advanced",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openAdvancedTabDialog(resource.uuid));
+ }
+ },
+ ]
+];
import copy from 'copy-to-clipboard';
import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
import { Link } from "components/icon/icon";
-import { getClipboardUrl } from "./helpers";
+import { getCollectionItemClipboardUrl } from "./helpers";
export const CopyToClipboardAction = (props: { href?: any, download?: any, onClick?: () => void, kind?: string, currentCollectionUuid?: string; }) => {
const copyToClipboard = () => {
if (props.href) {
- const clipboardUrl = getClipboardUrl(props.href, true, true);
+ const clipboardUrl = getCollectionItemClipboardUrl(props.href, true, true);
copy(clipboardUrl);
}
//
// SPDX-License-Identifier: AGPL-3.0
-import { sanitizeToken, getClipboardUrl, getInlineFileUrl } from "./helpers";
+import { sanitizeToken, getCollectionItemClipboardUrl, getInlineFileUrl } from "./helpers";
describe('helpers', () => {
// given
describe('getClipboardUrl', () => {
it('should add redirectTo query param', () => {
// when
- const result = getClipboardUrl(url);
+ const result = getCollectionItemClipboardUrl(url);
// then
expect(result).toBe('http://localhost?redirectToDownload=https://example.com/c=zzzzz-4zz18-0123456789abcde/LIMS/1.html');
return `${[prefix, ...rest].join('/')}${tokenAsQueryParam ? `${sep}api_token=${token}` : ''}`;
};
-export const getClipboardUrl = (href: string, shouldSanitizeToken = true, inline = false): string => {
+/**
+ * @returns A shareable token-free WB2 url that redirects to keep-web after login
+ */
+export const getCollectionItemClipboardUrl = (href: string, shouldSanitizeToken = true, inline = false): string => {
const { origin } = window.location;
const url = shouldSanitizeToken ? sanitizeToken(href, false) : href;
const redirectKey = inline ? REDIRECT_TO_PREVIEW_KEY : REDIRECT_TO_DOWNLOAD_KEY;
PERMISSION_EDIT = "PermissionEdit",
LINK = "Link",
WORKFLOW = "Workflow",
+ SEARCH_RESULTS = "SearchResults"
}
import { DispatchProp } from 'react-redux';
import { saveApiToken } from 'store/auth/auth-action';
import { navigateToRootProject } from 'store/navigation/navigation-action';
+import { replace } from 'react-router-redux';
type CssRules = 'root' | 'loginBtn' | 'card' | 'wrapper' | 'progress';
setSubmitting(false);
if (response.data.uuid && response.data.api_token) {
const apiToken = `v2/${response.data.uuid}/${response.data.api_token}`;
+ const rd = new URL(window.location.href);
+ const rdUrl = rd.pathname + rd.search;
dispatch<any>(saveApiToken(apiToken)).finally(
- () => dispatch(navigateToRootProject));
+ () => rdUrl === '/' ? dispatch(navigateToRootProject) : dispatch(replace(rdUrl))
+ );
} else {
setError(true);
setHelperText(response.data.message || 'Please try again');
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { Dialog, DialogActions, Button, StyleRulesCallback, WithStyles, withStyles, Tooltip, IconButton, CardHeader } from '@material-ui/core';
-import { withDialog } from "store/dialog/with-dialog";
-import { PROCESS_COMMAND_DIALOG_NAME } from 'store/processes/process-command-actions';
-import { WithDialogProps } from 'store/dialog/with-dialog';
-import { ProcessCommandDialogData } from 'store/processes/process-command-actions';
-import { DefaultCodeSnippet } from "components/default-code-snippet/default-code-snippet";
-import { compose } from 'redux';
-import CopyToClipboard from "react-copy-to-clipboard";
-import { CopyIcon } from 'components/icon/icon';
-
-type CssRules = 'codeSnippet' | 'copyToClipboard';
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
- codeSnippet: {
- marginLeft: theme.spacing.unit * 3,
- marginRight: theme.spacing.unit * 3,
- },
- copyToClipboard: {
- marginRight: theme.spacing.unit,
- }
-});
-
-export const ProcessCommandDialog = compose(
- withDialog(PROCESS_COMMAND_DIALOG_NAME),
- withStyles(styles),
-)(
- (props: WithDialogProps<ProcessCommandDialogData> & WithStyles<CssRules>) =>
- <Dialog
- open={props.open}
- maxWidth="md"
- onClose={props.closeDialog}
- style={{ alignSelf: 'stretch' }}>
- <CardHeader
- title={`Command - ${props.data.processName}`}
- action={
- <Tooltip title="Copy to clipboard">
- <CopyToClipboard text={props.data.command}>
- <IconButton className={props.classes.copyToClipboard}>
- <CopyIcon />
- </IconButton>
- </CopyToClipboard>
- </Tooltip>
- } />
- <DefaultCodeSnippet
- className={props.classes.codeSnippet}
- lines={[props.data.command]} />
- <DialogActions>
- <Button
- variant='text'
- color='primary'
- onClick={props.closeDialog}>
- Close
- </Button>
- </DialogActions>
- </Dialog>
-);
\ No newline at end of file
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
+ Paper,
StyleRulesCallback,
Typography,
withStyles,
| 'error'
| 'errorColor'
| 'warning'
- | 'warningColor';
+ | 'warningColor'
+ | 'paperRoot';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
warningColor: {
color: theme.customs.colors.yellow900,
},
+ paperRoot: {
+ minHeight: theme.spacing.unit * 6,
+ display: 'flex',
+ alignItems: 'center',
+ },
});
export interface ProcessRuntimeStatusDataProps {
runtimeStatus: RuntimeStatus | undefined;
+ containerCount: number;
}
type ProcessRuntimeStatusProps = ProcessRuntimeStatusDataProps & WithStyles<CssRules>;
export const ProcessRuntimeStatus = withStyles(styles)(
- ({ runtimeStatus, classes }: ProcessRuntimeStatusProps) => {
+ ({ runtimeStatus, containerCount, classes }: ProcessRuntimeStatusProps) => {
return <div className={classes.root}>
{ runtimeStatus?.error &&
<div data-cy='process-runtime-status-error'><ExpansionPanel className={classes.error} elevation={0}>
</ExpansionPanelDetails>
</ExpansionPanel></div>
}
+ { containerCount > 1 &&
+ <div data-cy='process-runtime-status-retry-warning' >
+ <Paper className={classNames(classes.warning, classes.paperRoot)} elevation={0}>
+ <Typography className={classNames(classes.heading, classes.summary, classes.warningColor)}>
+ {`Warning: Process retried ${containerCount - 1} time${containerCount > 2 ? 's' : ''} due to failure.`}
+ </Typography>
+ </Paper>
+ </div>
+ }
</div>
-});
\ No newline at end of file
+});
const List = withStyles(styles)(
({ classes, handleDelete, properties }: ResourcePropertiesListProps) =>
- <div>
+ <div data-cy="resource-properties-list">
{properties &&
Object.keys(properties).map(k =>
Array.isArray(properties[k])
(dispatch: Dispatch): ResourcePropertiesListActionProps => ({
handleDelete: (key: string, value: string) => dispatch<any>(removePropertyFromResourceForm(key, value, formName))
})
- )(List);
\ No newline at end of file
+ )(List);
import Adapter from 'enzyme-adapter-react-16';
import CopyToClipboard from 'react-copy-to-clipboard';
import { TokenDialogComponent } from './token-dialog';
+import { combineReducers, createStore } from 'redux';
+import { Provider } from 'react-redux';
configure({ adapter: new Adapter() });
describe('<CurrentTokenDialog />', () => {
let props;
let wrapper;
+ let store;
beforeEach(() => {
props = {
open: true,
dispatch: jest.fn(),
};
+
+ const initialAuthState = {
+ localCluster: "zzzzz",
+ remoteHostsConfig: {},
+ sessions: {},
+ };
+
+ store = createStore(combineReducers({
+ auth: (state: any = initialAuthState, action: any) => state,
+ }));
});
describe('Get API Token dialog', () => {
beforeEach(() => {
- wrapper = mount(<TokenDialogComponent {...props} />);
+ wrapper = mount(
+ <Provider store={store}>
+ <TokenDialogComponent {...props} />
+ </Provider>
+ );
});
it('should include API host and token', () => {
const someDate = '2140-01-01T00:00:00.000Z'
props.tokenExpiration = new Date(someDate);
- wrapper = mount(<TokenDialogComponent {...props} />);
+ wrapper = mount(
+ <Provider store={store}>
+ <TokenDialogComponent {...props} />
+ </Provider>);
expect(wrapper.html()).toContain(props.tokenExpiration.toLocaleString());
});
expect(wrapper.html()).not.toContain('GET NEW TOKEN');
props.canCreateNewTokens = true;
- wrapper = mount(<TokenDialogComponent {...props} />);
+ wrapper = mount(
+ <Provider store={store}>
+ <TokenDialogComponent {...props} />
+ </Provider>);
expect(wrapper.html()).toContain('GET NEW TOKEN');
});
});
describe('copy to clipboard button', () => {
beforeEach(() => {
- wrapper = mount(<TokenDialogComponent {...props} />);
+ wrapper = mount(
+ <Provider store={store}>
+ <TokenDialogComponent {...props} />
+ </Provider>);
});
it('should copy API TOKEN to the clipboard', () => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import {
+ StyleRulesCallback,
+ WithStyles,
+ withStyles,
+ Card,
+ CardHeader,
+ IconButton,
+ CardContent,
+ Tooltip,
+ Typography,
+ Grid,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { CloseIcon, CommandIcon, CopyIcon } from 'components/icon/icon';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+import { DefaultCodeSnippet } from 'components/default-code-snippet/default-code-snippet';
+import { Process } from 'store/processes/process';
+import shellescape from 'shell-escape';
+import CopyToClipboard from 'react-copy-to-clipboard';
+
+type CssRules = 'card' | 'content' | 'title' | 'header' | 'avatar' | 'iconHeader';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ card: {
+ height: '100%'
+ },
+ header: {
+ paddingTop: theme.spacing.unit,
+ paddingBottom: theme.spacing.unit,
+ },
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.green700,
+ },
+ avatar: {
+ alignSelf: 'flex-start',
+ paddingTop: theme.spacing.unit * 0.5
+ },
+ content: {
+ padding: theme.spacing.unit * 1.0,
+ paddingTop: theme.spacing.unit * 0.5,
+ '&:last-child': {
+ paddingBottom: theme.spacing.unit * 1,
+ }
+ },
+ title: {
+ overflow: 'hidden',
+ paddingTop: theme.spacing.unit * 0.5
+ },
+});
+
+interface ProcessCmdCardDataProps {
+ process: Process;
+ onCopy: (text: string) => void;
+}
+
+type ProcessCmdCardProps = ProcessCmdCardDataProps & WithStyles<CssRules> & MPVPanelProps;
+
+export const ProcessCmdCard = withStyles(styles)(
+ ({
+ process,
+ onCopy,
+ classes,
+ doHidePanel,
+ }: ProcessCmdCardProps) => {
+ const command = process.containerRequest.command.map((v) =>
+ shellescape([v]) // Escape each arg separately
+ );
+
+ let formattedCommand = [...command];
+ formattedCommand.forEach((item, i, arr) => {
+ // Indent lines after the first
+ const indent = i > 0 ? ' ' : '';
+ // Escape newlines on every non-last arg when there are multiple lines
+ const lineBreak = arr.length > 1 && i < arr.length - 1 ? ' \\' : '';
+ arr[i] = `${indent}${item}${lineBreak}`;
+ });
+
+ return (
+ <Card className={classes.card}>
+ <CardHeader
+ className={classes.header}
+ classes={{
+ content: classes.title,
+ avatar: classes.avatar,
+ }}
+ avatar={<CommandIcon className={classes.iconHeader} />}
+ title={
+ <Typography noWrap variant="h6" color="inherit">
+ Command
+ </Typography>
+ }
+ action={
+ <Grid container direction="row" alignItems="center">
+ <Grid item>
+ <Tooltip title="Copy to clipboard" disableFocusListener>
+ <IconButton>
+ <CopyToClipboard
+ text={command.join(" ")}
+ onCopy={() => onCopy("Command copied to clipboard")}
+ >
+ <CopyIcon />
+ </CopyToClipboard>
+ </IconButton>
+ </Tooltip>
+ </Grid>
+ <Grid item>
+ {doHidePanel && (
+ <Tooltip
+ title={`Close Command Panel`}
+ disableFocusListener
+ >
+ <IconButton onClick={doHidePanel}>
+ <CloseIcon />
+ </IconButton>
+ </Tooltip>
+ )}
+ </Grid>
+ </Grid>
+ }
+ />
+ <CardContent className={classes.content}>
+ <DefaultCodeSnippet lines={formattedCommand} linked />
+ </CardContent>
+ </Card>
+ );
+ }
+);
const mdSize = props.twoCol ? 6 : 12;
return <Grid container>
<Grid item xs={12}>
- <ProcessRuntimeStatus runtimeStatus={container?.runtimeStatus} />
+ <ProcessRuntimeStatus runtimeStatus={container?.runtimeStatus} containerCount={containerRequest.containerCount} />
</Grid>
{!props.hideProcessPanelRedundantFields && <Grid item xs={12} md={mdSize}>
<DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
MaximizeIcon,
TextDecreaseIcon,
TextIncreaseIcon,
- WordWrapIcon
+ WordWrapOffIcon,
+ WordWrapOnIcon,
} from 'components/icon/icon';
import { Process } from 'store/processes/process';
import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
({ classes, process, filters, selectedFilter, lines,
onLogFilterChange, navigateToLog, onCopy,
doHidePanel, doMaximizePanel, panelMaximized, panelName }: ProcessLogsCardProps) => {
- const [wordWrapToggle, setWordWrapToggle] = useState<boolean>(true);
+ const [wordWrap, setWordWrap] = useState<boolean>(true);
const [fontSize, setFontSize] = useState<number>(3);
const fontBaseSize = 10;
const fontStepSize = 1;
</Tooltip>
</Grid>
<Grid item>
- <Tooltip title="Toggle word wrapping" disableFocusListener>
- <IconButton onClick={() => setWordWrapToggle(!wordWrapToggle)}>
- <WordWrapIcon />
+ <Tooltip title={`${wordWrap ? 'Disable' : 'Enable'} word wrapping`} disableFocusListener>
+ <IconButton onClick={() => setWordWrap(!wordWrap)}>
+ {wordWrap ? <WordWrapOffIcon /> : <WordWrapOnIcon />}
</IconButton>
</Tooltip>
</Grid>
spacing={24}
direction='column'>
<Grid className={classes.logViewer} item xs>
- <ProcessLogCodeSnippet fontSize={fontBaseSize+(fontStepSize*fontSize)} wordWrap={wordWrapToggle} lines={lines} />
+ <ProcessLogCodeSnippet fontSize={fontBaseSize+(fontStepSize*fontSize)} wordWrap={wordWrap} lines={lines} />
</Grid>
</Grid>
: <DefaultView
import grey from '@material-ui/core/colors/grey';
import { ArvadosTheme } from 'common/custom-theme';
import { Link, Typography } from '@material-ui/core';
-import { navigateTo } from 'store/navigation/navigation-action';
+import { navigationNotAvailable } from 'store/navigation/navigation-action';
import { Dispatch } from 'redux';
import { connect, DispatchProp } from 'react-redux';
import classNames from 'classnames';
+import { FederationConfig, getNavUrl } from 'routes/routes';
+import { RootState } from 'store/store';
type CssRules = 'root' | 'wordWrap' | 'logText';
overflow: 'auto',
backgroundColor: '#000',
height: `calc(100% - ${theme.spacing.unit * 4}px)`, // so that horizontal scollbar is visible
+ "& a": {
+ color: theme.palette.primary.main,
+ },
},
logText: {
padding: theme.spacing.unit * 0.5,
wordWrap?: boolean;
}
-const renderLinks = (fontSize: number, dispatch: Dispatch) => (text: string) => {
+interface ProcessLogCodeSnippetAuthProps {
+ auth: FederationConfig;
+}
+
+const renderLinks = (fontSize: number, auth: FederationConfig, dispatch: Dispatch) => (text: string) => {
// Matches UUIDs & PDHs
const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
const links = text.match(REGEX);
<React.Fragment key={index}>
{part}
{links[index] &&
- <Link onClick={() => dispatch<any>(navigateTo(links[index]))}
+ <Link onClick={() => {
+ const url = getNavUrl(links[index], auth)
+ if (url) {
+ window.open(`${window.location.origin}${url}`, '_blank');
+ } else {
+ dispatch(navigationNotAvailable(links[index]));
+ }
+ }}
style={ {cursor: 'pointer'} }>
{links[index]}
</Link>}
</Typography>;
};
-export const ProcessLogCodeSnippet = withStyles(styles)(connect()(
- ({classes, lines, fontSize, dispatch, wordWrap}: ProcessLogCodeSnippetProps & WithStyles<CssRules> & DispatchProp) => {
- const [followMode, setFollowMode] = useState<boolean>(false);
+const mapStateToProps = (state: RootState): ProcessLogCodeSnippetAuthProps => ({
+ auth: state.auth,
+});
+
+export const ProcessLogCodeSnippet = withStyles(styles)(connect(mapStateToProps)(
+ ({classes, lines, fontSize, auth, dispatch, wordWrap}: ProcessLogCodeSnippetProps & WithStyles<CssRules> & ProcessLogCodeSnippetAuthProps & DispatchProp) => {
+ const [followMode, setFollowMode] = useState<boolean>(true);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
<div ref={scrollRef} className={classes.root}
onScroll={(e) => {
const elem = e.target as HTMLDivElement;
- if (elem.scrollTop + elem.clientHeight >= elem.scrollHeight) {
+ if (elem.scrollTop + (elem.clientHeight*1.1) >= elem.scrollHeight) {
setFollowMode(true);
} else {
setFollowMode(false);
{ lines.map((line: string, index: number) =>
<Typography key={index} component="pre"
className={classNames(classes.logText, wordWrap ? classes.wordWrap : undefined)}>
- {renderLinks(fontSize, dispatch)(line)}
+ {renderLinks(fontSize, auth, dispatch)(line)}
</Typography>
) }
</div>
import { getProcessPanelLogs, ProcessLogsPanel } from 'store/process-logs-panel/process-logs-panel';
import { ProcessLogsCard } from './process-log-card';
import { FilterOption } from 'views/process-panel/process-log-form';
+import { ProcessCmdCard } from './process-cmd-card';
type CssRules = 'root';
cancelProcess: (uuid: string) => void;
onLogFilterChange: (filter: FilterOption) => void;
navigateToLog: (uuid: string) => void;
- onLogCopyToClipboard: (uuid: string) => void;
+ onCopyToClipboard: (uuid: string) => void;
}
export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles<CssRules>;
const panelsData: MPVPanelState[] = [
{name: "Details"},
+ {name: "Command"},
{name: "Logs", visible: true},
{name: "Subprocesses"},
];
cancelProcess={props.cancelProcess}
/>
</MPVPanelContent>
+ <MPVPanelContent forwardProps xs="auto" data-cy="process-cmd">
+ <ProcessCmdCard
+ onCopy={props.onCopyToClipboard}
+ process={process} />
+ </MPVPanelContent>
<MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-logs">
<ProcessLogsCard
- onCopy={props.onLogCopyToClipboard}
+ onCopy={props.onCopyToClipboard}
process={process}
lines={getProcessPanelLogs(processLogsPanel)}
selectedFilter={{
};
const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
- onLogCopyToClipboard: (message: string) => {
+ onCopyToClipboard: (message: string) => {
dispatch<any>(snackbarActions.OPEN_SNACKBAR({
message,
hideDuration: 2000,
import { Dispatch } from "redux";
import { connect } from "react-redux";
import { navigateTo } from 'store/navigation/navigation-action';
-// import { openContextMenu, resourceKindToContextMenuKind } from 'store/context-menu/context-menu-actions';
-// import { ResourceKind } from 'models/resource';
+import { openSearchResultsContextMenu } from 'store/context-menu/context-menu-actions';
import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
import { SearchResultsPanelView } from 'views/search-results-panel/search-results-panel-view';
import { RootState } from 'store/store';
};
const mapDispatchToProps = (dispatch: Dispatch): SearchResultsPanelActionProps => ({
- onContextMenu: (event, resourceUuid) => { return; },
+ onContextMenu: (event, resourceUuid) => {
+ dispatch<any>(openSearchResultsContextMenu(event, resourceUuid));
+ },
onDialogOpen: (ownerUuid: string) => { return; },
onItemClick: (resourceUuid: string) => {
dispatch<any>(loadDetailsPanel(resourceUuid));
import { MoveCollectionDialog } from 'views-components/dialog-forms/move-collection-dialog';
import { FilesUploadCollectionDialog } from 'views-components/dialog-forms/files-upload-collection-dialog';
import { PartialCopyCollectionDialog } from 'views-components/dialog-forms/partial-copy-collection-dialog';
-import { ProcessCommandDialog } from 'views-components/process-command-dialog/process-command-dialog';
import { RemoveProcessDialog } from 'views-components/process-remove-dialog/process-remove-dialog';
import { MainContentBar } from 'views-components/main-content-bar/main-content-bar';
import { Grid } from '@material-ui/core';
<PublicKeyDialog />
<PartialCopyCollectionDialog />
<PartialCopyToCollectionDialog />
- <ProcessCommandDialog />
<ProcessInputDialog />
<RestoreCollectionVersionDialog />
<RemoveApiClientAuthorizationDialog />
-type test \
-config ${TMPDIR}/arvados.yml \
-no-workbench1 \
+ -no-workbench2 \
-own-temporary-database \
-timeout 20m 2> ${ARVADOS_LOG})
trap cleanup ERR EXIT
languageName: node
linkType: hard
-"@types/shell-quote@npm:1.6.0":
- version: 1.6.0
- resolution: "@types/shell-quote@npm:1.6.0"
- checksum: 5d9f4e35c8df32d9994f8ae2f1a1fe8a6b7ee96794f803e0904ceae7ad7255a214954e85cd75bd847fe77458d3746430522e87237438f223b7d72a23c4928c0e
+"@types/shell-escape@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "@types/shell-escape@npm:0.2.0"
+ checksum: 020696ed313eeb02deb2abcc581e8b570be6f9ee662892339965b524bb4fbdc9a97b6520d914117740ec11147b0b1aa52358b8e03fa214c2da99743adb196853
languageName: node
linkType: hard
languageName: node
linkType: hard
-"array-filter@npm:~0.0.0":
- version: 0.0.1
- resolution: "array-filter@npm:0.0.1"
- checksum: 0e9afdf5e248c45821c6fe1232071a13a3811e1902c2c2a39d12e4495e8b0b25739fd95bffbbf9884b9693629621f6077b4ae16207b8f23d17710fc2465cebbb
- languageName: node
- linkType: hard
-
"array-find-index@npm:^1.0.1":
version: 1.0.2
resolution: "array-find-index@npm:1.0.2"
languageName: node
linkType: hard
-"array-map@npm:~0.0.0":
- version: 0.0.0
- resolution: "array-map@npm:0.0.0"
- checksum: 30d73fdc99956c8bd70daea40db5a7d78c5c2c75a03c64fc77904885e79adf7d5a0595076534f4e58962d89435f0687182ac929e65634e3d19931698cbac8149
- languageName: node
- linkType: hard
-
-"array-reduce@npm:~0.0.0":
- version: 0.0.0
- resolution: "array-reduce@npm:0.0.0"
- checksum: d6226325271f477e3dd65b4d40db8597735b8d08bebcca4972e52d3c173d6c697533664fa8865789ea2d076bdaf1989bab5bdfbb61598be92074a67f13057c3a
- languageName: node
- linkType: hard
-
"array-union@npm:^1.0.1":
version: 1.0.2
resolution: "array-union@npm:1.0.2"
"@types/redux-devtools": 3.0.44
"@types/redux-form": 7.4.12
"@types/redux-mock-store": 1.0.2
- "@types/shell-quote": 1.6.0
+ "@types/shell-escape": ^0.2.0
"@types/sinon": 7.5
"@types/uuid": 3.4.4
axios: ^0.21.1
redux-thunk: 2.3.0
reselect: 4.0.0
set-value: 2.0.1
- shell-quote: 1.6.1
+ shell-escape: ^0.2.0
sinon: 7.3
ts-mock-imports: 1.3.7
tslint: 5.20.0
languageName: node
linkType: hard
-"shell-quote@npm:1.6.1":
- version: 1.6.1
- resolution: "shell-quote@npm:1.6.1"
- dependencies:
- array-filter: ~0.0.0
- array-map: ~0.0.0
- array-reduce: ~0.0.0
- jsonify: ~0.0.0
- checksum: 982a4fdf2d474f0dc40885de4222f100ba457d7c75d46b532bf23b01774b8617bc62522c6825cb1fa7dd4c54c18e9dcbae7df2ca8983101841b6f2e6a7cacd2f
+"shell-escape@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "shell-escape@npm:0.2.0"
+ checksum: 0d87f1ae22ad22a74e148348ceaf64721e3024f83c90afcfb527318ce10ece654dd62e103dd89a242f2f4e4ce3cecdef63e3d148c40e5fabca8ba6c508f97d9f
languageName: node
linkType: hard