docker run --env ci="true" \
--env ARVADOS_DIRECTORY=/tmp/arvados \
--env APP_NAME=${APP_NAME} \
+ --env VERSION="${VERSION}" \
--env ITERATION=${ITERATION} \
--env TARGETS="${TARGETS}" \
+ --env MAINTAINER="${MAINTAINER}" \
+ --env DESCRIPTION="${DESCRIPTION}" \
-w="/tmp/workbench2" \
-t -v ${WORKSPACE}:/tmp/workbench2 \
-v ${ARVADOS_DIRECTORY}:/tmp/arvados workbench2-build:latest \
});
});
+ it("preserves original ordering of lines within the same log type", function () {
+ const crName = "test_container_request";
+ createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
+ cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
+ // Should come first
+ "2023-07-18T20:14:46.000000000Z A out 1",
+ // Comes fourth in a contiguous block
+ "2023-07-18T20:14:48.128642814Z A out 2",
+ "2023-07-18T20:14:48.128642814Z X out 3",
+ "2023-07-18T20:14:48.128642814Z A out 4",
+ ]).as("stdout");
+
+ cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
+ // Comes second
+ "2023-07-18T20:14:47.000000000Z Z err 1",
+ // Comes third in a contiguous block
+ "2023-07-18T20:14:48.128642814Z B err 2",
+ "2023-07-18T20:14:48.128642814Z C err 3",
+ "2023-07-18T20:14:48.128642814Z Y err 4",
+ "2023-07-18T20:14:48.128642814Z Z err 5",
+ "2023-07-18T20:14:48.128642814Z A err 6",
+ ]).as("stderr");
+
+ cy.loginAs(activeUser);
+ cy.goToPath(`/processes/${containerRequest.uuid}`);
+ cy.get("[data-cy=process-details]").should("contain", crName);
+ cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
+
+ cy.getAll("@stdout", "@stderr").then(() => {
+ // Switch to All logs
+ cy.get("[data-cy=process-logs-filter]").click();
+ cy.get("body").contains("li", "All logs").click();
+ // Verify sorted logs
+ cy.get("[data-cy=process-logs] pre").eq(0).should("contain", "2023-07-18T20:14:46.000000000Z A out 1");
+ cy.get("[data-cy=process-logs] pre").eq(1).should("contain", "2023-07-18T20:14:47.000000000Z Z err 1");
+ cy.get("[data-cy=process-logs] pre").eq(2).should("contain", "2023-07-18T20:14:48.128642814Z B err 2");
+ cy.get("[data-cy=process-logs] pre").eq(3).should("contain", "2023-07-18T20:14:48.128642814Z C err 3");
+ cy.get("[data-cy=process-logs] pre").eq(4).should("contain", "2023-07-18T20:14:48.128642814Z Y err 4");
+ cy.get("[data-cy=process-logs] pre").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z Z err 5");
+ cy.get("[data-cy=process-logs] pre").eq(6).should("contain", "2023-07-18T20:14:48.128642814Z A err 6");
+ cy.get("[data-cy=process-logs] pre").eq(7).should("contain", "2023-07-18T20:14:48.128642814Z A out 2");
+ cy.get("[data-cy=process-logs] pre").eq(8).should("contain", "2023-07-18T20:14:48.128642814Z X out 3");
+ cy.get("[data-cy=process-logs] pre").eq(9).should("contain", "2023-07-18T20:14:48.128642814Z A out 4");
+ });
+ });
+ });
+
it("correctly generates sniplines", function () {
const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
const crName = "test_container_request";
});
});
});
+
+ it('collapses and un-collapses', () => {
+
+ cy.loginAs(activeUser)
+ cy.get('[data-cy=side-panel-tree]').should('exist')
+ cy.get('[data-cy=side-panel-toggle]').click()
+ cy.get('[data-cy=side-panel-tree]').should('not.exist')
+ cy.get('[data-cy=side-panel-collapsed]').should('exist')
+ cy.get('[data-cy=side-panel-toggle]').click()
+ cy.get('[data-cy=side-panel-tree]').should('exist')
+ cy.get('[data-cy=side-panel-collapsed]').should('not.exist')
+ })
+
+ it('can navigate from collapsed panel', () => {
+
+ const collapsedCategories = {
+ 'shared-with-me': '/shared-with-me',
+ 'public-favorites': '/public-favorites',
+ 'my-favorites': '/favorites',
+ 'groups': '/groups',
+ 'all-processes': '/all_processes',
+ 'trash': '/trash',
+ 'shell-access': '/virtual-machines-user',
+ 'home-projects': `/projects/${activeUser.user.uuid}`,
+ }
+
+ cy.loginAs(activeUser)
+ cy.get('[data-cy=side-panel-tree]').should('exist')
+ cy.get('[data-cy=side-panel-toggle]').click()
+ cy.get('[data-cy=side-panel-collapsed]').should('exist')
+
+ for (const cat in collapsedCategories) {
+ cy.get(`[data-cy=collapsed-${cat}]`).should('exist').click()
+ cy.url().should('include', collapsedCategories[cat])
+ }
+ })
})
+
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@coreui/coreui": "^4.3.2",
+ "@coreui/react": "^4.11.0",
"@date-io/date-fns": "1",
"@fortawesome/fontawesome-svg-core": "1.2.28",
"@fortawesome/free-solid-svg-icons": "5.13.0",
"axios": "^0.21.1",
"babel-core": "6.26.3",
"babel-runtime": "6.26.0",
+ "bootstrap": "^5.3.2",
"caniuse-lite": "1.0.30001299",
"classnames": "2.2.6",
"cwlts": "1.15.29",
"material-ui-pickers": "^2.2.4",
"mem": "4.0.0",
"mime": "^3.0.0",
- "moment": "2.29.1",
+ "moment": "^2.29.4",
"parse-duration": "0.4.4",
"prop-types": "15.7.2",
"query-string": "6.9.0",
- "react": "16.8.6",
+ "react": "16.14.0",
"react-copy-to-clipboard": "5.0.3",
"react-dnd": "5.0.0",
"react-dnd-html5-backend": "5.0.1",
- "react-dom": "16.8.6",
+ "react-dom": "16.14.0",
"react-dropzone": "5.1.1",
"react-highlight-words": "0.14.0",
"react-idle-timer": "4.3.6",
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- version="1.1"
- id="svg148"
- width="300"
- height="300"
- viewBox="0 0 300 300"
- xml:space="preserve"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:svg="http://www.w3.org/2000/svg"><defs
- id="defs152" /><g
- id="g154"><g
- id="g6337"><path
- style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:22.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
- d="m 191.30938,11.567958 0.0193,275.898262"
- id="path400" /><path
- style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:22.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
- d="m 202.57626,149.50744 -89.79939,0.0193"
- id="path400-3" /><path
- style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:59.0707;stroke-dasharray:none;stroke-opacity:1"
- id="path4546"
- d="M 81.113348,90.153499 -22.761723,90.479332 28.893633,0.35796487 Z"
- transform="matrix(0,0.5047589,0.28743877,-0.01237225,93.434122,136.22641)" /></g></g></svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M80-240v-480h80v480H80Zm560 0-57-56 144-144H240v-80h487L584-664l56-56 240 240-240 240Z"/></svg>
\ No newline at end of file
import { PaperProps } from "@material-ui/core/Paper";
import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
-type CssRules = "searchBox" | "headerMenu" | "toolbar" | "footer" | "root" | "moreOptionsButton" | "title" | "dataTable" | "container";
+type CssRules = "titleWrapper" | "searchBox" | "headerMenu" | "toolbar" | "footer" | "root" | "moreOptionsButton" | "title" | 'subProcessTitle' | "dataTable" | "container";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ titleWrapper: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
searchBox: {
paddingBottom: 0,
},
toolbar: {
paddingTop: 0,
paddingRight: theme.spacing.unit,
+ paddingLeft: "10px",
},
footer: {
overflow: "auto",
paddingLeft: theme.spacing.unit * 2,
paddingTop: theme.spacing.unit * 2,
fontSize: "18px",
+ paddingRight: "10px",
+ },
+ subProcessTitle: {
+ display: "inline-block",
+ paddingLeft: theme.spacing.unit * 2,
+ paddingTop: theme.spacing.unit * 2,
+ fontSize: "18px",
+ flexGrow: 0,
+ paddingRight: "10px",
},
dataTable: {
height: "100%",
height: "100%",
},
headerMenu: {
- width: "100%",
- float: "right",
- display: "flex",
- flexDirection: "row-reverse",
- justifyContent: "space-between",
+ marginLeft: "auto",
+ flexBasis: "initial",
+ flexGrow: 0,
},
});
actions?: React.ReactNode;
hideSearchInput?: boolean;
title?: React.ReactNode;
+ progressBar?: React.ReactNode;
paperKey?: string;
currentItemUuid: string;
elementPath?: string;
prevRoute: "",
};
+ multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
+
componentDidUpdate(prevProps: DataExplorerProps<T>) {
const currentRefresh = this.props.currentRefresh || "";
const currentRoute = this.props.currentRoute || "";
paperKey,
fetchMode,
currentItemUuid,
+ currentRoute,
title,
+ progressBar,
doHidePanel,
doMaximizePanel,
doUnMaximizePanel,
wrap="nowrap"
className={classes.container}
>
- <div>
+ <div className={classes.titleWrapper} style={currentRoute?.includes('search-results') || !!progressBar ? {marginBottom: '-20px'} : {}}>
{title && (
<Grid
item
xs
- className={classes.title}
+ className={!!progressBar ? classes.subProcessTitle : classes.title}
>
{title}
</Grid>
)}
+ {!!progressBar && progressBar}
+ {this.multiSelectToolbarInTitle && <MultiselectToolbar />}
{(!hideColumnSelector || !hideSearchInput || !!actions) && (
<Grid
className={classes.headerMenu}
xs
>
<Toolbar className={classes.toolbar}>
- {!hideSearchInput && (
- <div className={classes.searchBox}>
- {!hideSearchInput && (
- <SearchInput
- label={searchLabel}
- value={searchValue}
- selfClearProp={""}
- onSearch={onSearch}
- />
- )}
- </div>
- )}
- {actions}
- {!hideColumnSelector && (
- <ColumnSelector
- columns={columns}
- onColumnToggle={onColumnToggle}
- />
- )}
+ <Grid container justify="space-between" wrap="nowrap" alignItems="center">
+ {!hideSearchInput && (
+ <div className={classes.searchBox}>
+ {!hideSearchInput && (
+ <SearchInput
+ label={searchLabel}
+ value={searchValue}
+ selfClearProp={""}
+ onSearch={onSearch}
+ />
+ )}
+ </div>
+ )}
+ {actions}
+ {!hideColumnSelector && (
+ <ColumnSelector
+ columns={columns}
+ onColumnToggle={onColumnToggle}
+ />
+ )}
+ </Grid>
{doUnMaximizePanel && panelMaximized && (
<Tooltip
title={`Unmaximize ${panelName || "panel"}`}
</Tooltip>
)}
</Toolbar>
- <MultiselectToolbar />
</Grid>
)}
</div>
+ {!this.multiSelectToolbarInTitle && <MultiselectToolbar />}
<Grid
item
xs="auto"
className={classes.dataTable}
+ style={currentRoute?.includes('search-results') || !!progressBar ? {marginTop: '-10px'} : {}}
>
<DataTable
columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
</SvgIcon>
);
+//https://pictogrammers.com/library/mdi/icon/console/
+export const TerminalIcon: IconType = (props: any) => (
+ <SvgIcon {...props}>
+ <path d="M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z" />
+ </SvgIcon>
+)
+
export type IconType = React.SFC<{ className?: string; style?: object }>;
export const AddIcon: IconType = props => <Add {...props} />;
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
display: "flex",
+ flexShrink: 0,
flexDirection: "row",
width: 0,
height: '2.7rem',
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { configure, mount } from "enzyme";
+import { ServiceRepository, createServices } from "services/services";
+import { configureStore } from "store/store";
+import { createBrowserHistory } from "history";
+import { mockConfig } from 'common/config';
+import { ApiActions } from "services/api/api-actions";
+import Axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import { Process } from "store/processes/process";
+import { ContainerState } from "models/container";
+import Adapter from "enzyme-adapter-react-16";
+import { SubprocessProgressBar } from "./subprocess-progress-bar";
+import { Provider } from "react-redux";
+import { FilterBuilder } from 'services/api/filter-builder';
+import { ProcessStatusFilter, buildProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
+import {act} from "react-dom/test-utils";
+
+configure({ adapter: new Adapter() });
+
+describe("<SubprocessProgressBar />", () => {
+ const axiosInst = Axios.create({ headers: {} });
+ const axiosMock = new MockAdapter(axiosInst);
+
+ let store;
+ let services: ServiceRepository;
+ const config: any = {};
+ const actions: ApiActions = {
+ progressFn: (id: string, working: boolean) => { },
+ errorFn: (id: string, message: string) => { }
+ };
+ let statusResponse = {
+ [ProcessStatusFilter.COMPLETED]: 0,
+ [ProcessStatusFilter.RUNNING]: 0,
+ [ProcessStatusFilter.FAILED]: 0,
+ [ProcessStatusFilter.QUEUED]: 0,
+ };
+
+ const createMockListFunc = (uuid: string) => jest.fn(async (args) => {
+ const baseFilter = new FilterBuilder().addEqual('requesting_container_uuid', uuid).getFilters();
+
+ const filterResponses = Object.keys(statusResponse)
+ .map(status => ({filters: buildProcessStatusFilters(new FilterBuilder(baseFilter), status).getFilters(), value: statusResponse[status]}));
+
+ const matchedFilter = filterResponses.find(response => response.filters === args.filters);
+ if (matchedFilter) {
+ return { itemsAvailable: matchedFilter.value };
+ } else {
+ return { itemsAvailable: 0 };
+ }
+ });
+
+ beforeEach(() => {
+ services = createServices(mockConfig({}), actions, axiosInst);
+ store = configureStore(createBrowserHistory(), services, config);
+ });
+
+ it("requests subprocess progress stats for stopped processes and displays progress", async () => {
+ // when
+ const process = {
+ container: {
+ state: ContainerState.COMPLETE,
+ },
+ containerRequest: {
+ containerUuid: 'zzzzz-dz642-000000000000000',
+ },
+ } as Process;
+
+ statusResponse = {
+ [ProcessStatusFilter.COMPLETED]: 100,
+ [ProcessStatusFilter.RUNNING]: 200,
+
+ // Combined into failed segment
+ [ProcessStatusFilter.FAILED]: 200,
+ [ProcessStatusFilter.CANCELLED]: 100,
+
+ // Combined into queued segment
+ [ProcessStatusFilter.QUEUED]: 300,
+ [ProcessStatusFilter.ONHOLD]: 100,
+ };
+
+ services.containerRequestService.list = createMockListFunc(process.containerRequest.containerUuid);
+
+ let progressBar;
+ await act(async () => {
+ progressBar = mount(
+ <Provider store={store}>
+ <SubprocessProgressBar process={process} />
+ </Provider>);
+ });
+ await progressBar.update();
+
+ // expects 6 subprocess status list requests
+ const expectedFilters = [
+ ProcessStatusFilter.COMPLETED,
+ ProcessStatusFilter.RUNNING,
+ ProcessStatusFilter.FAILED,
+ ProcessStatusFilter.CANCELLED,
+ ProcessStatusFilter.QUEUED,
+ ProcessStatusFilter.ONHOLD,
+ ].map((state) =>
+ buildProcessStatusFilters(
+ new FilterBuilder().addEqual(
+ "requesting_container_uuid",
+ process.containerRequest.containerUuid
+ ),
+ state
+ ).getFilters()
+ );
+
+ expectedFilters.forEach((filter) => {
+ expect(services.containerRequestService.list).toHaveBeenCalledWith({limit: 0, offset: 0, filters: filter});
+ });
+
+ // Verify progress bar with correct degment widths
+ ['10%', '20%', '30%', '40%'].forEach((value, i) => {
+ const styles = progressBar.find('.progress').at(i).props().style;
+ expect(styles).toHaveProperty('width', value);
+ });
+ });
+
+ it("dislays correct progress bar widths with different values", async () => {
+ const process = {
+ container: {
+ state: ContainerState.COMPLETE,
+ },
+ containerRequest: {
+ containerUuid: 'zzzzz-dz642-000000000000001',
+ },
+ } as Process;
+
+ statusResponse = {
+ [ProcessStatusFilter.COMPLETED]: 50,
+ [ProcessStatusFilter.RUNNING]: 55,
+
+ [ProcessStatusFilter.FAILED]: 30,
+ [ProcessStatusFilter.CANCELLED]: 30,
+
+ [ProcessStatusFilter.QUEUED]: 235,
+ [ProcessStatusFilter.ONHOLD]: 100,
+ };
+
+ services.containerRequestService.list = createMockListFunc(process.containerRequest.containerUuid);
+
+ let progressBar;
+ await act(async () => {
+ progressBar = mount(
+ <Provider store={store}>
+ <SubprocessProgressBar process={process} />
+ </Provider>);
+ });
+ await progressBar.update();
+
+ // Verify progress bar with correct degment widths
+ ['10%', '11%', '12%', '67%'].forEach((value, i) => {
+ const styles = progressBar.find('.progress').at(i).props().style;
+ expect(styles).toHaveProperty('width', value);
+ });
+ });
+
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { useEffect, useState } from "react";
+import { StyleRulesCallback, Tooltip, WithStyles, withStyles } from "@material-ui/core";
+import { CProgressStacked, CProgress } from '@coreui/react';
+import { useAsyncInterval } from "common/use-async-interval";
+import { Process, isProcessRunning } from "store/processes/process";
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+import { fetchSubprocessProgress } from "store/subprocess-panel/subprocess-panel-actions";
+import { ProcessStatusFilter } from "store/resource-type-filters/resource-type-filters";
+
+type CssRules = 'progressWrapper' | 'progressStacked' ;
+
+const styles: StyleRulesCallback<CssRules> = (theme) => ({
+ progressWrapper: {
+ margin: "28px 0 0",
+ flexGrow: 1,
+ flexBasis: "100px",
+ },
+ progressStacked: {
+ border: "1px solid gray",
+ height: "10px",
+ // Override stripe color to be close to white
+ "& .progress-bar-striped": {
+ backgroundImage:
+ "linear-gradient(45deg,rgba(255,255,255,.80) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.80) 50%,rgba(255,255,255,.80) 75%,transparent 75%,transparent)",
+ },
+ },
+});
+
+export interface ProgressBarDataProps {
+ process: Process;
+}
+
+export interface ProgressBarActionProps {
+ fetchSubprocessProgress: (requestingContainerUuid: string) => Promise<ProgressBarData | undefined>;
+}
+
+type ProgressBarProps = ProgressBarDataProps & ProgressBarActionProps & WithStyles<CssRules>;
+
+export type ProgressBarData = {
+ [ProcessStatusFilter.COMPLETED]: number;
+ [ProcessStatusFilter.RUNNING]: number;
+ [ProcessStatusFilter.FAILED]: number;
+ [ProcessStatusFilter.QUEUED]: number;
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): ProgressBarActionProps => ({
+ fetchSubprocessProgress: (requestingContainerUuid: string) => {
+ return dispatch<any>(fetchSubprocessProgress(requestingContainerUuid));
+ },
+});
+
+export const SubprocessProgressBar = connect(null, mapDispatchToProps)(withStyles(styles)(
+ ({process, classes, fetchSubprocessProgress}: ProgressBarProps) => {
+
+ const [progressData, setProgressData] = useState<ProgressBarData|undefined>(undefined);
+ const requestingContainerUuid = process.containerRequest.containerUuid;
+ const isRunning = isProcessRunning(process);
+
+ useAsyncInterval(async () => (
+ requestingContainerUuid && setProgressData(await fetchSubprocessProgress(requestingContainerUuid))
+ ), isRunning ? 5000 : null);
+
+ useEffect(() => {
+ if (!isRunning && requestingContainerUuid) {
+ fetchSubprocessProgress(requestingContainerUuid)
+ .then(result => setProgressData(result));
+ }
+ }, [fetchSubprocessProgress, isRunning, requestingContainerUuid]);
+
+ return progressData !== undefined && getStatusTotal(progressData) > 0 ? <div className={classes.progressWrapper}>
+ <CProgressStacked className={classes.progressStacked}>
+ <Tooltip title={`${progressData[ProcessStatusFilter.COMPLETED]} Completed`}>
+ <CProgress height={10} color="success"
+ value={getStatusPercent(progressData, ProcessStatusFilter.COMPLETED)} />
+ </Tooltip>
+ <Tooltip title={`${progressData[ProcessStatusFilter.RUNNING]} Running`}>
+ <CProgress height={10} color="success" variant="striped"
+ value={getStatusPercent(progressData, ProcessStatusFilter.RUNNING)} />
+ </Tooltip>
+ <Tooltip title={`${progressData[ProcessStatusFilter.FAILED]} Failed`}>
+ <CProgress height={10} color="danger"
+ value={getStatusPercent(progressData, ProcessStatusFilter.FAILED)} />
+ </Tooltip>
+ <Tooltip title={`${progressData[ProcessStatusFilter.QUEUED]} Queued`}>
+ <CProgress height={10} color="secondary" variant="striped"
+ value={getStatusPercent(progressData, ProcessStatusFilter.QUEUED)} />
+ </Tooltip>
+ </CProgressStacked>
+ </div> : <></>;
+ }
+));
+
+const getStatusTotal = (progressData: ProgressBarData) =>
+ (Object.keys(progressData).reduce((accumulator, key) => (accumulator += progressData[key]), 0));
+
+/**
+ * Gets the integer percent value for process status
+ */
+const getStatusPercent = (progressData: ProgressBarData, status: keyof ProgressBarData) =>
+ (progressData[status] / getStatusTotal(progressData) * 100);
import React, { useCallback, useState } from 'react';
import { List, ListItem, ListItemIcon, Checkbox, Radio, Collapse } from "@material-ui/core";
import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
-import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon, FilterGroupIcon, FreezeIcon } from 'components/icon/icon';
+import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon, ProcessIcon, FilterGroupIcon, FreezeIcon } from 'components/icon/icon';
import { ReactElement } from "react";
import CircularProgress from '@material-ui/core/CircularProgress';
import classnames from "classnames";
import { SidePanelRightArrowIcon } from '../icon/icon';
import { ResourceKind } from 'models/resource';
import { GroupClass } from 'models/group';
+import { SidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions';
type CssRules = 'list'
| 'listItem'
| 'checkbox'
| 'childItem'
| 'childItemIcon'
- | 'frozenIcon';
+ | 'frozenIcon'
+ | 'indentSpacer';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
list: {
color: theme.palette.grey["700"],
height: '14px',
width: '14px',
+ marginBottom: '0.4rem',
},
toggableIcon: {
- fontSize: '14px'
+ fontSize: '14px',
},
renderContainer: {
flex: 1
color: theme.palette.grey["600"],
marginLeft: '10px',
},
+ indentSpacer: {
+ width: '0.25rem'
+ }
});
export enum TreeItemStatus {
export interface TreeItem<T> {
data: T;
+ depth?: number;
id: string;
open: boolean;
active: boolean;
return [action, id];
};
+const isInFavoritesTree = (item: TreeItem<any>): boolean => {
+ return item.id === SidePanelTreeCategory.FAVORITES || item.id === SidePanelTreeCategory.PUBLIC_FAVORITES;
+}
+
interface FlatTreeProps {
it: TreeItem<any>;
levelIndentation: number;
toggleActive: 'TOGGLE_ACTIVE',
};
-const ItemIcon = React.memo(({ type, kind, active, groupClass, classes }: any) => {
+const ItemIcon = React.memo(({ type, kind, headKind, active, groupClass, classes }: any) => {
let Icon = ProjectIcon;
if (groupClass === GroupClass.FILTER) {
}
if (kind) {
+ if(kind === ResourceKind.LINK && headKind) kind = headKind;
switch (kind) {
case ResourceKind.COLLECTION:
Icon = CollectionIcon;
break;
+ case ResourceKind.CONTAINER_REQUEST:
+ Icon = ProcessIcon;
+ break;
default:
break;
}
.map((item: any) => <div key={item.id} data-id={item.id}
className={classnames(props.classes.childItem, { [props.classes.active]: item.active })}
style={{ paddingLeft: `${item.depth * props.levelIndentation}px` }}>
- <i data-action={FLAT_TREE_ACTIONS.toggleOpen} className={props.classes.toggableIconContainer}>
- <ListItemIcon className={props.getToggableIconClassNames(item.open, item.active)}>
- {props.getProperArrowAnimation(item.status, item.items!)}
- </ListItemIcon>
- </i>
+ {isInFavoritesTree(props.it) ?
+ <div className={props.classes.indentSpacer} />
+ :
+ <i data-action={FLAT_TREE_ACTIONS.toggleOpen} className={props.classes.toggableIconContainer}>
+ <ListItemIcon className={props.getToggableIconClassNames(item.open, item.active)}>
+ {props.getProperArrowAnimation(item.status, item.items!)}
+ </ListItemIcon>
+ </i>}
{props.showSelection(item) && !props.useRadioButtons &&
<Checkbox
checked={item.selected}
color="primary" />}
<div data-action={FLAT_TREE_ACTIONS.toggleActive} className={props.classes.renderContainer} ref={item.active ? props.selectedRef : undefined}>
<span style={{ display: 'flex', alignItems: 'center' }}>
- <ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} groupClass={item.data.kind === ResourceKind.GROUP ? item.data.groupClass : ''} classes={props.classes} />
+ <ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} headKind={item.data.headKind || null} groupClass={item.data.kind === ResourceKind.GROUP ? item.data.groupClass : ''} classes={props.classes} />
<span style={{ fontSize: '0.875rem' }}>
{item.data.name}
</span>
const { levelIndentation = 20, itemRightPadding = 20 } = props;
return <List className={list}>
{items && items.map((it: TreeItem<T>, idx: number) => {
+ if (isInFavoritesTree(it) && it.open === true && it.items && it.items.length) {
+ it = { ...it, items: it.items.filter(item => item.depth && item.depth < 3) }
+ }
return <div key={`item/${level}/${it.id}`}>
<ListItem button className={listItem}
style={{
import { storeRedirects } from "./common/redirect-to";
import { searchResultsActionSet } from "views-components/context-menu/action-sets/search-results-action-set";
+import 'bootstrap/dist/css/bootstrap.min.css';
+import '@coreui/coreui/dist/css/coreui.min.css';
+
console.log(`Starting arvados [${getBuildInfo()}]`);
addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
+import { loadFavoritesTree} from "store/side-panel-tree/side-panel-tree-actions";
export const favoritesActions = unionize({
TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
}));
dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_FAVORITES))
dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
+ dispatch<any>(loadFavoritesTree())
})
.catch((e: any) => {
dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
case SidePanelTreeCategory.ALL_PROCESSES:
dispatch(navigateToAllProcesses);
return;
+ case SidePanelTreeCategory.SHELL_ACCESS:
+ dispatch(navigateToUserVirtualMachines)
+ return;
case USERS_PANEL_LABEL:
dispatch(navigateToUsers);
return;
lastByte: number;
}
+type SortableLine = {
+ logType: LogEventType,
+ timestamp: string;
+ contents: string;
+}
+
export type ProcessLogsPanelAction = UnionOf<typeof processLogsPanelActions>;
export const setProcessLogsPanelFilter = (filter: string) =>
* @returns string[] of merged and sorted log lines
*/
const mergeSortLogFragments = (logFragments: LogFragment[]): string[] => {
- const sortableLines = fragmentsToLines(logFragments
- .filter((fragment) => (!NON_SORTED_LOG_TYPES.includes(fragment.logType))));
+ const sortableFragments = logFragments
+ .filter((fragment) => (!NON_SORTED_LOG_TYPES.includes(fragment.logType)));
const nonSortableLines = fragmentsToLines(logFragments
.filter((fragment) => (NON_SORTED_LOG_TYPES.includes(fragment.logType)))
.sort((a, b) => (a.logType.localeCompare(b.logType))));
- return [...nonSortableLines, ...sortableLines.sort(sortLogLines)]
+ return [...nonSortableLines, ...sortLogFragments(sortableFragments)];
};
-const sortLogLines = (a: string, b: string) => {
- return a.localeCompare(b);
+/**
+ * Performs merge and sort of input log fragment lines
+ * @param logFragments set of sortable log fragments to be merged and sorted
+ * @returns A string array containing all lines, sorted by timestamp and
+ * preserving line ordering and type grouping when timestamps match
+ */
+const sortLogFragments = (logFragments: LogFragment[]): string[] => {
+ const linesWithType: SortableLine[] = logFragments
+ // Map each logFragment into an array of SortableLine
+ .map((fragment: LogFragment): SortableLine[] => (
+ fragment.contents.map((singleLine: string) => {
+ const timestampMatch = singleLine.match(LOG_TIMESTAMP_PATTERN);
+ const timestamp = timestampMatch && timestampMatch[0] ? timestampMatch[0] : "";
+ return {
+ logType: fragment.logType,
+ timestamp: timestamp,
+ contents: singleLine,
+ };
+ })
+ // Merge each array of SortableLine into single array
+ )).reduce((acc: SortableLine[], lines: SortableLine[]) => (
+ [...acc, ...lines]
+ ), [] as SortableLine[]);
+
+ return linesWithType
+ .sort(sortableLineSortFunc)
+ .map(lineWithType => lineWithType.contents);
+};
+
+/**
+ * Sort func to sort lines
+ * Preserves original ordering of lines from the same source
+ * Stably orders lines of differing type but same timestamp
+ * (produces a block of same-timestamped lines of one type before a block
+ * of same timestamped lines of another type for readability)
+ * Sorts all other lines by contents (ie by timestamp)
+ */
+const sortableLineSortFunc = (a: SortableLine, b: SortableLine) => {
+ if (a.logType === b.logType) {
+ return 0;
+ } else if (a.timestamp === b.timestamp) {
+ return a.logType.localeCompare(b.logType);
+ } else {
+ return a.contents.localeCompare(b.contents);
+ }
};
const fragmentsToLines = (fragments: LogFragment[]): string[] => (
import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { loadPublicFavoritesTree } from "store/side-panel-tree/side-panel-tree-actions";
export const publicFavoritesActions = unionize({
TOGGLE_PUBLIC_FAVORITE: ofType<{ resourceUuid: string }>(),
}));
dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_PUBLIC_FAVORITES))
dispatch(progressIndicatorActions.STOP_WORKING("togglePublicFavorite"));
+ dispatch<any>(loadPublicFavoritesTree())
})
.catch((e: any) => {
dispatch(progressIndicatorActions.STOP_WORKING("togglePublicFavorite"));
import { ProjectResource } from 'models/project';
import { OrderBuilder } from 'services/api/order-builder';
import { ResourceKind } from 'models/resource';
-import { GroupContentsResourcePrefix } from 'services/groups-service/groups-service';
-import { GroupClass } from 'models/group';
import { CategoriesListReducer } from 'common/plugintypes';
import { pluginConfig } from 'plugins';
+import { LinkClass } from 'models/link';
export enum SidePanelTreeCategory {
PROJECTS = 'Home Projects',
- SHARED_WITH_ME = 'Shared with me',
- PUBLIC_FAVORITES = 'Public Favorites',
FAVORITES = 'My Favorites',
- TRASH = 'Trash',
+ PUBLIC_FAVORITES = 'Public Favorites',
+ SHARED_WITH_ME = 'Shared with me',
ALL_PROCESSES = 'All Processes',
+ SHELL_ACCESS = 'Shell Access',
GROUPS = 'Groups',
+ TRASH = 'Trash',
}
export const SIDE_PANEL_TREE = 'sidePanelTree';
+const SIDEPANEL_TREE_NODE_LIMIT = 50
export const getSidePanelTree = (treePicker: TreePicker) =>
getTreePicker<ProjectResource | string>(SIDE_PANEL_TREE)(treePicker);
let SIDE_PANEL_CATEGORIES: string[] = [
SidePanelTreeCategory.PROJECTS,
- SidePanelTreeCategory.SHARED_WITH_ME,
- SidePanelTreeCategory.PUBLIC_FAVORITES,
SidePanelTreeCategory.FAVORITES,
- SidePanelTreeCategory.GROUPS,
+ SidePanelTreeCategory.PUBLIC_FAVORITES,
+ SidePanelTreeCategory.SHARED_WITH_ME,
SidePanelTreeCategory.ALL_PROCESSES,
+ SidePanelTreeCategory.SHELL_ACCESS,
+ SidePanelTreeCategory.GROUPS,
SidePanelTreeCategory.TRASH
];
nodes
}));
SIDE_PANEL_CATEGORIES.forEach(category => {
- if (category !== SidePanelTreeCategory.PROJECTS && category !== SidePanelTreeCategory.SHARED_WITH_ME) {
+ if (category !== SidePanelTreeCategory.PROJECTS && category !== SidePanelTreeCategory.FAVORITES && category !== SidePanelTreeCategory.PUBLIC_FAVORITES ) {
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
id: category,
pickerId: SIDE_PANEL_TREE,
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const treePicker = getTreePicker(SIDE_PANEL_TREE)(getState().treePicker);
const node = treePicker ? getNode(projectUuid)(treePicker) : undefined;
- if (projectUuid === SidePanelTreeCategory.SHARED_WITH_ME) {
- await dispatch<any>(loadSharedRoot);
+ if (projectUuid === SidePanelTreeCategory.PUBLIC_FAVORITES) {
+ await dispatch<any>(loadPublicFavoritesTree());
+ } else if (projectUuid === SidePanelTreeCategory.FAVORITES) {
+ await dispatch<any>(loadFavoritesTree());
} else if (node || projectUuid !== '') {
await dispatch<any>(loadProject(projectUuid));
}
.addEqual('owner_uuid', projectUuid)
.getFilters(),
order: new OrderBuilder<ProjectResource>()
- .addAsc('name')
- .getOrder()
+ .addDesc('createdAt')
+ .getOrder(),
+ limit: SIDEPANEL_TREE_NODE_LIMIT,
};
+
const { items } = await services.projectService.list(params);
+
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
id: projectUuid,
pickerId: SIDE_PANEL_TREE,
dispatch(resourcesActions.SET_RESOURCES(items));
};
-const loadSharedRoot = async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: SidePanelTreeCategory.SHARED_WITH_ME, pickerId: SIDE_PANEL_TREE }));
+export const loadFavoritesTree = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: SidePanelTreeCategory.FAVORITES, pickerId: SIDE_PANEL_TREE }));
+
+ const params = {
+ filters: new FilterBuilder()
+ .addEqual('link_class', LinkClass.STAR)
+ .addEqual('tail_uuid', getUserUuid(getState()))
+ .addEqual('tail_kind', ResourceKind.USER)
+ .getFilters(),
+ order: new OrderBuilder<ProjectResource>().addDesc('createdAt').getOrder(),
+ limit: SIDEPANEL_TREE_NODE_LIMIT,
+ };
+
+ const { items } = await services.linkService.list(params);
+
+ dispatch(
+ treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ id: SidePanelTreeCategory.FAVORITES,
+ pickerId: SIDE_PANEL_TREE,
+ nodes: items.map(item => initTreeNode({ id: item.headUuid, value: item })),
+ })
+ );
+
+ dispatch(resourcesActions.SET_RESOURCES(items));
+};
+
+export const loadPublicFavoritesTree = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: SidePanelTreeCategory.PUBLIC_FAVORITES, pickerId: SIDE_PANEL_TREE }));
+
+ const uuidPrefix = getState().auth.config.uuidPrefix;
+ const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
+ const typeFilters = [ResourceKind.COLLECTION, ResourceKind.CONTAINER_REQUEST, ResourceKind.GROUP, ResourceKind.WORKFLOW];
const params = {
- filters: `[${new FilterBuilder()
- .addIsA('uuid', ResourceKind.PROJECT)
- .addIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
- .addDistinct('uuid', getState().auth.config.uuidPrefix + '-j7d0g-publicfavorites')
- .getFilters()}]`,
- order: new OrderBuilder<ProjectResource>()
- .addAsc('name', GroupContentsResourcePrefix.PROJECT)
- .getOrder(),
- limit: 1000
+ filters: new FilterBuilder()
+ .addEqual('link_class', LinkClass.STAR)
+ .addEqual('owner_uuid', publicProjectUuid)
+ .addIsA('head_uuid', typeFilters)
+ .getFilters(),
+ order: new OrderBuilder<ProjectResource>().addDesc('createdAt').getOrder(),
+ limit: SIDEPANEL_TREE_NODE_LIMIT,
};
- const { items } = await services.groupsService.shared(params);
+ const { items } = await services.linkService.list(params);
- dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
- id: SidePanelTreeCategory.SHARED_WITH_ME,
- pickerId: SIDE_PANEL_TREE,
- nodes: items.map(item => initTreeNode({ id: item.uuid, value: item })),
- }));
+ dispatch(
+ treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ id: SidePanelTreeCategory.PUBLIC_FAVORITES,
+ pickerId: SIDE_PANEL_TREE,
+ nodes: items.map(item => initTreeNode({ id: item.headUuid, value: item })),
+ })
+ );
dispatch(resourcesActions.SET_RESOURCES(items));
};
async (dispatch: Dispatch, getState: () => RootState) => {
const node = getSidePanelTreeNode(id)(getState().treePicker);
if (node && !node.active) {
- dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SIDE_PANEL_TREE }));
- }
- if (!isSidePanelTreeCategory(id)) {
+ dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SIDE_PANEL_TREE }));
+ }
+ if (!isSidePanelTreeCategory(id)) {
await dispatch<any>(activateSidePanelTreeProject(id));
}
};
const userUuid = getUserUuid(getState());
if (!userUuid) { return; }
const ancestors = await services.ancestorsService.ancestors(id, userUuid);
- const isShared = ancestors.every(({ uuid }) => uuid !== userUuid);
- if (isShared) {
- await dispatch<any>(loadSidePanelTreeProjects(SidePanelTreeCategory.SHARED_WITH_ME));
- }
for (const ancestor of ancestors) {
await dispatch<any>(loadSidePanelTreeProjects(ancestor.uuid));
}
dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
- ids: [
- ...(isShared ? [SidePanelTreeCategory.SHARED_WITH_ME] : []),
- ...ancestors.map(ancestor => ancestor.uuid)
- ],
+ ids: ancestors.map(ancestor => ancestor.uuid),
pickerId: SIDE_PANEL_TREE
}));
dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SIDE_PANEL_TREE }));
if (node && node.status === TreeNodeStatus.INITIAL) {
await dispatch<any>(loadSidePanelTreeProjects(node.id));
}
- dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId: SIDE_PANEL_TREE }));
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId: SIDE_PANEL_TREE }));
};
export const expandSidePanelTreeItem = (id: string) =>
import { RootState } from 'store/store';
import { ServiceRepository } from 'services/services';
import { bindDataExplorerActions } from 'store/data-explorer/data-explorer-action';
+import { FilterBuilder } from 'services/api/filter-builder';
+import { ProgressBarData } from 'components/subprocess-progress-bar/subprocess-progress-bar';
+import { ProcessStatusFilter, buildProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
export const SUBPROCESS_PANEL_ID = "subprocessPanel";
export const SUBPROCESS_ATTRIBUTES_DIALOG = 'subprocessAttributesDialog';
export const subprocessPanelActions = bindDataExplorerActions(SUBPROCESS_PANEL_ID);
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(subprocessPanelActions.REQUEST_ITEMS());
};
+
+/**
+ * Holds a ProgressBarData status type and process count result
+ */
+type ProcessStatusBarCount = {
+ status: keyof ProgressBarData;
+ count: number;
+};
+
+/**
+ * Associates each of the limited progress bar segment types with an array of
+ * ProcessStatusFilterTypes to be combined when displayed
+ */
+type ProcessStatusMap = Record<keyof ProgressBarData, ProcessStatusFilter[]>;
+
+const statusMap: ProcessStatusMap = {
+ [ProcessStatusFilter.COMPLETED]: [ProcessStatusFilter.COMPLETED],
+ [ProcessStatusFilter.RUNNING]: [ProcessStatusFilter.RUNNING],
+ [ProcessStatusFilter.FAILED]: [ProcessStatusFilter.FAILED, ProcessStatusFilter.CANCELLED],
+ [ProcessStatusFilter.QUEUED]: [ProcessStatusFilter.QUEUED, ProcessStatusFilter.ONHOLD],
+};
+
+/**
+ * Utility type to hold a pair of associated progress bar status and process status
+ */
+type ProgressBarStatusPair = {
+ barStatus: keyof ProcessStatusMap;
+ processStatus: ProcessStatusFilter;
+};
+
+export const fetchSubprocessProgress = (requestingContainerUuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<ProgressBarData | undefined> => {
+
+ const requestContainerStatusCount = async (fb: FilterBuilder) => {
+ return await services.containerRequestService.list({
+ limit: 0,
+ offset: 0,
+ filters: fb.getFilters(),
+ });
+ }
+
+ if (requestingContainerUuid) {
+ try {
+ const baseFilter = new FilterBuilder().addEqual('requesting_container_uuid', requestingContainerUuid).getFilters();
+
+ // Create return object
+ let result: ProgressBarData = {
+ [ProcessStatusFilter.COMPLETED]: 0,
+ [ProcessStatusFilter.RUNNING]: 0,
+ [ProcessStatusFilter.FAILED]: 0,
+ [ProcessStatusFilter.QUEUED]: 0,
+ }
+
+ // Create array of promises that returns the status associated with the item count
+ // Helps to make the requests simultaneously while preserving the association with the status key as a typed key
+ const promises = (Object.keys(statusMap) as Array<keyof ProcessStatusMap>)
+ // Split statusMap into pairs of progress bar status and process status
+ .reduce((acc, curr) => [...acc, ...statusMap[curr].map(processStatus => ({barStatus: curr, processStatus}))], [] as ProgressBarStatusPair[])
+ .map(async (statusPair: ProgressBarStatusPair): Promise<ProcessStatusBarCount> => {
+ // For each status pair, request count and return bar status and count
+ const { barStatus, processStatus } = statusPair;
+ const filter = buildProcessStatusFilters(new FilterBuilder(baseFilter), processStatus);
+ const count = (await requestContainerStatusCount(filter)).itemsAvailable;
+ return {status: barStatus, count};
+ });
+
+ // Simultaneously requests each status count and apply them to the return object
+ (await Promise.all(promises)).forEach((singleResult) => {
+ result[singleResult.status] += singleResult.count;
+ });
+ return result;
+ } catch (e) {
+ return undefined;
+ }
+ } else {
+ return undefined;
+ }
+ };
let store: RootStore;
let services: ServiceRepository;
- const config: any = {
-
-
- };
+ const config: any = {};
const actions: ApiActions = {
progressFn: (id: string, working: boolean) => { },
errorFn: (id: string, message: string) => { }
import { loadTrashPanel, trashPanelActions } from "store/trash-panel/trash-panel-action";
import { loadProcessPanel } from "store/process-panel/process-panel-actions";
import { loadSharedWithMePanel, sharedWithMePanelActions } from "store/shared-with-me-panel/shared-with-me-panel-actions";
+import { sharedWithMePanelColumns } from "views/shared-with-me-panel/shared-with-me-panel";
import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
import { workflowPanelActions } from "store/workflow-panel/workflow-panel-actions";
import { loadSshKeysPanel } from "store/auth/auth-action-ssh";
})
);
dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
- dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
+ dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: sharedWithMePanelColumns }));
dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
dispatch(
searchResultsPanelActions.SET_FETCH_MODE({
);
});
+const _resourceWithNameLink = withStyles(
+ {},
+ { withTheme: true }
+)((props: { uuid: string; userFullname: string; dispatch: Dispatch; theme: ArvadosTheme }) => {
+ const { uuid, userFullname, dispatch, theme } = props;
+ if (!userFullname) {
+ dispatch<any>(loadResource(uuid, false));
+ }
+
+ return (
+ <Typography
+ style={{ color: theme.palette.primary.main, cursor: 'pointer' }}
+ inline
+ noWrap
+ onClick={() => dispatch<any>(navigateTo(uuid))}
+ >
+ {userFullname ? userFullname : uuid}
+ </Typography>
+ )
+});
+
+
+export const ResourceOwnerWithNameLink = ownerFromResourceId(_resourceWithNameLink);
+
export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
export const ResourceWithName = userFromID(_resourceWithName);
root: {
width: `${COLLAPSE_ICON_SIZE}px`,
height: `${COLLAPSE_ICON_SIZE}px`,
- marginTop: '0.4rem'
+ marginTop: '0.4rem',
+ marginLeft: '0.7rem',
+ paddingTop: '1rem',
+ paddingRight: '1rem'
},
icon: {
- height: '1.5rem',
- width: '3rem',
- opacity: '0.6',
+ opacity: '0.5',
},
}
return <Tooltip disableFocusListener title="Toggle Side Panel">
- <IconButton style={collapseButtonIconStyles.root} onClick={() => { props.toggleSidePanel(props.isCollapsed) }}>
+ <IconButton data-cy="side-panel-toggle" style={collapseButtonIconStyles.root} onClick={() => { props.toggleSidePanel(props.isCollapsed) }}>
<div>
{props.isCollapsed ?
- <img style={{ ...collapseButtonIconStyles.icon, transform: "rotate(180deg)" }} src='/collapseLHS-New.svg#svgView(preserveAspectRatio(none))' alt='expand button' />
+ <img style={{...collapseButtonIconStyles.icon, marginLeft:'0.25rem'}} src='/mui-start-icon.svg' alt='an arrow pointing right'/>
:
- <img style={{ ...collapseButtonIconStyles.icon, }} src='/collapseLHS-New.svg#svgView(preserveAspectRatio(none))' alt='collapse button' />}
+ <img style={{ ...collapseButtonIconStyles.icon, transform: "rotate(180deg)"}} src='/mui-start-icon.svg' alt='an arrow pointing right'/>}
</div>
</IconButton>
</Tooltip>
import { TreeItem } from "components/tree/tree";
import { ProjectResource } from "models/project";
import { ListItemTextIcon } from "components/list-item-text-icon/list-item-text-icon";
-import { ProcessIcon, ProjectIcon, FilterGroupIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon, GroupsIcon } from 'components/icon/icon';
+import { ProcessIcon, ProjectIcon, FilterGroupIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon, GroupsIcon, TerminalIcon } from 'components/icon/icon';
import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions';
import { openSidePanelContextMenu } from 'store/context-menu/context-menu-actions';
import { noop } from 'lodash';
? getSidePanelIcon(item.data)
: (item.data && item.data.groupClass === GroupClass.FILTER)
? FilterGroupIcon
- : ProjectIcon;
+ : ProjectsIcon;
export const getSidePanelIcon = (category: string) => {
switch (category) {
return ProcessIcon;
case SidePanelTreeCategory.GROUPS:
return GroupsIcon;
+ case SidePanelTreeCategory.SHELL_ACCESS:
+ return TerminalIcon
default:
return ProjectIcon;
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { ReactElement } from 'react'
+import { connect } from 'react-redux'
+import { ProjectsIcon, ProcessIcon, FavoriteIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon, GroupsIcon } from 'components/icon/icon'
+import { TerminalIcon } from 'components/icon/icon'
+import { IconButton, List, ListItem, Tooltip } from '@material-ui/core'
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'
+import { ArvadosTheme } from 'common/custom-theme'
+import { navigateTo } from 'store/navigation/navigation-action'
+import { RootState } from 'store/store'
+import { Dispatch } from 'redux'
+import {
+ navigateToSharedWithMe,
+ navigateToPublicFavorites,
+ navigateToFavorites,
+ navigateToGroups,
+ navigateToAllProcesses,
+ navigateToTrash,
+} from 'store/navigation/navigation-action'
+import { navigateToUserVirtualMachines } from 'store/navigation/navigation-action'
+import { RouterAction } from 'react-router-redux'
+import { User } from 'models/user'
+
+type CssRules = 'button' | 'unselected' | 'selected'
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ button: {
+ width: '40px',
+ height: '40px',
+ paddingLeft: '-2rem',
+ marginLeft: '-0.6rem',
+ marginBottom: '-1rem'
+ },
+ unselected: {
+ color: theme.customs.colors.grey600,
+ },
+ selected: {
+ color: theme.palette.primary.main,
+ },
+})
+
+enum SidePanelCollapsedCategory {
+ PROJECTS = 'Home Projects',
+ FAVORITES = 'My Favorites',
+ PUBLIC_FAVORITES = 'Public Favorites',
+ SHARED_WITH_ME = 'Shared with me',
+ ALL_PROCESSES = 'All Processes',
+ SHELL_ACCESS = 'Shell Access',
+ GROUPS = 'Groups',
+ TRASH = 'Trash',
+}
+
+type TCollapsedCategory = {
+ name: SidePanelCollapsedCategory
+ icon: ReactElement
+ navTarget: RouterAction | ''
+}
+
+const sidePanelCollapsedCategories: TCollapsedCategory[] = [
+ {
+ name: SidePanelCollapsedCategory.PROJECTS,
+ icon: <ProjectsIcon />,
+ navTarget: '',
+ },
+ {
+ name: SidePanelCollapsedCategory.FAVORITES,
+ icon: <FavoriteIcon />,
+ navTarget: navigateToFavorites,
+ },
+ {
+ name: SidePanelCollapsedCategory.PUBLIC_FAVORITES,
+ icon: <PublicFavoriteIcon />,
+ navTarget: navigateToPublicFavorites,
+ },
+ {
+ name: SidePanelCollapsedCategory.SHARED_WITH_ME,
+ icon: <ShareMeIcon />,
+ navTarget: navigateToSharedWithMe,
+ },
+ {
+ name: SidePanelCollapsedCategory.ALL_PROCESSES,
+ icon: <ProcessIcon />,
+ navTarget: navigateToAllProcesses,
+ },
+ {
+ name: SidePanelCollapsedCategory.SHELL_ACCESS,
+ icon: <TerminalIcon />,
+ navTarget: navigateToUserVirtualMachines,
+ },
+ {
+ name: SidePanelCollapsedCategory.GROUPS,
+ icon: <GroupsIcon style={{marginLeft: '2px', scale: '85%'}}/>,
+ navTarget: navigateToGroups,
+ },
+ {
+ name: SidePanelCollapsedCategory.TRASH,
+ icon: <TrashIcon />,
+ navTarget: navigateToTrash,
+ },
+]
+
+type SidePanelCollapsedProps = {
+ user: User;
+ selectedPath: string;
+ navToHome: (uuid: string) => void;
+ navTo: (navTarget: RouterAction | '') => void;
+};
+
+const mapStateToProps = ({auth, properties }: RootState) => {
+ return {
+ user: auth.user,
+ selectedPath: properties.breadcrumbs
+ ? properties.breadcrumbs[0].label !== 'Virtual Machines'
+ ? properties.breadcrumbs[0].label
+ : SidePanelCollapsedCategory.SHELL_ACCESS
+ : SidePanelCollapsedCategory.PROJECTS,
+ }
+}
+
+const mapDispatchToProps = (dispatch: Dispatch) => {
+ return {
+ navToHome: (navTarget) => dispatch<any>(navigateTo(navTarget)),
+ navTo: (navTarget) => dispatch<any>(navTarget),
+ }
+}
+
+export const SidePanelCollapsed = withStyles(styles)(
+ connect(mapStateToProps, mapDispatchToProps)(({ classes, user, selectedPath, navToHome, navTo }: WithStyles & SidePanelCollapsedProps) => {
+
+ const handleClick = (cat: TCollapsedCategory) => {
+ if (cat.name === SidePanelCollapsedCategory.PROJECTS) navToHome(user.uuid)
+ else navTo(cat.navTarget)
+ }
+
+ const { button, unselected, selected } = classes
+ return (
+ <List data-cy='side-panel-collapsed'>
+ {sidePanelCollapsedCategories.map((cat) => (
+ <ListItem
+ key={cat.name}
+ data-cy={`collapsed-${cat.name.toLowerCase().replace(/\s+/g, '-')}`}
+ onClick={() => handleClick(cat)}
+ >
+ <Tooltip
+ className={selectedPath === cat.name ? selected : unselected}
+ title={cat.name}
+ disableFocusListener
+ >
+ <IconButton className={button}>{cat.icon}</IconButton>
+ </Tooltip>
+ </ListItem>
+ ))}
+ </List>
+ );
+ })
+)
import { SidePanelButton } from 'views-components/side-panel-button/side-panel-button';
import { RootState } from 'store/store';
import SidePanelToggle from 'views-components/side-panel-toggle/side-panel-toggle';
+import { SidePanelCollapsed } from './side-panel-collapsed';
const DRAWER_WIDTH = 240;
connect(mapStateToProps, mapDispatchToProps)(
({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps & { currentRoute: string }) =>
<Grid item xs>
- {props.isCollapsed ? <SidePanelToggle /> :
+ {props.isCollapsed ?
+ <>
+ <SidePanelToggle />
+ <SidePanelCollapsed />
+ </>
+ :
<>
<Grid className={classes.topButtonContainer}>
<SidePanelButton key={props.currentRoute} />
xs
maxHeight="50%"
data-cy="process-children">
- <SubprocessPanel />
+ <SubprocessPanel process={process} />
</MPVPanelContent>
</MPVContainer>
) : (
import { ArvadosTheme } from 'common/custom-theme';
import { ShareMeIcon } from 'components/icon/icon';
import { ResourcesState, getResource } from 'store/resources/resources';
+import { ResourceKind } from 'models/resource';
import { navigateTo } from "store/navigation/navigation-action";
import { loadDetailsPanel } from "store/details-panel/details-panel-action";
import { SHARED_WITH_ME_PANEL_ID } from 'store/shared-with-me-panel/shared-with-me-panel-actions';
openContextMenu,
resourceUuidToContextMenuKind
} from 'store/context-menu/context-menu-actions';
+import {
+ ResourceName,
+ ProcessStatus as ResourceStatus,
+ ResourceType,
+ ResourceOwnerWithNameLink,
+ ResourcePortableDataHash,
+ ResourceFileSize,
+ ResourceFileCount,
+ ResourceUUID,
+ ResourceContainerUuid,
+ ContainerRunTime,
+ ResourceOutputUuid,
+ ResourceLogUuid,
+ ResourceParentProcess,
+ ResourceModifiedByUserUuid,
+ ResourceVersion,
+ ResourceCreatedAtDate,
+ ResourceLastModifiedDate,
+ ResourceTrashDate,
+ ResourceDeleteDate,
+} from 'views-components/data-explorer/renderers';
+import { DataTableFilterItem } from 'components/data-table-filters/data-table-filters';
import { GroupContentsResource } from 'services/groups-service/groups-service';
import { toggleOne } from 'store/multiselect/multiselect-actions';
+import { DataColumns } from 'components/data-table/data-table';
+import { ContainerRequestState } from 'models/container-request';
+import { ProjectResource } from 'models/project';
+import { createTree } from 'models/tree';
+import { SortDirection } from 'components/data-table/data-column';
+import { getInitialResourceTypeFilters, getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
type CssRules = "toolbar" | "button" | "root";
},
});
+export enum SharedWithMePanelColumnNames {
+ NAME = 'Name',
+ STATUS = 'Status',
+ TYPE = 'Type',
+ OWNER = 'Owner',
+ PORTABLE_DATA_HASH = 'Portable Data Hash',
+ FILE_SIZE = 'File Size',
+ FILE_COUNT = 'File Count',
+ UUID = 'UUID',
+ CONTAINER_UUID = 'Container UUID',
+ RUNTIME = 'Runtime',
+ OUTPUT_UUID = 'Output UUID',
+ LOG_UUID = 'Log UUID',
+ PARENT_PROCESS = 'Parent Process UUID',
+ MODIFIED_BY_USER_UUID = 'Modified by User UUID',
+ VERSION = 'Version',
+ CREATED_AT = 'Date Created',
+ LAST_MODIFIED = 'Last Modified',
+ TRASH_AT = 'Trash at',
+ DELETE_AT = 'Delete at',
+}
+
+export interface ProjectPanelFilter extends DataTableFilterItem {
+ type: ResourceKind | ContainerRequestState;
+}
+
+export const sharedWithMePanelColumns: DataColumns<string, ProjectResource> = [
+ {
+ name: SharedWithMePanelColumnNames.NAME,
+ selected: true,
+ configurable: true,
+ sort: { direction: SortDirection.NONE, field: 'name' },
+ filters: createTree(),
+ render: (uuid) => <ResourceName uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.STATUS,
+ selected: true,
+ configurable: true,
+ mutuallyExclusiveFilters: true,
+ filters: getInitialProcessStatusFilters(),
+ render: (uuid) => <ResourceStatus uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.TYPE,
+ selected: true,
+ configurable: true,
+ filters: getInitialResourceTypeFilters(),
+ render: (uuid) => <ResourceType uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.OWNER,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceOwnerWithNameLink uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.PORTABLE_DATA_HASH,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourcePortableDataHash uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.FILE_SIZE,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceFileSize uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.FILE_COUNT,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceFileCount uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.UUID,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceUUID uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.CONTAINER_UUID,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceContainerUuid uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.RUNTIME,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ContainerRunTime uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.OUTPUT_UUID,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceOutputUuid uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.LOG_UUID,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceLogUuid uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.PARENT_PROCESS,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceParentProcess uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.MODIFIED_BY_USER_UUID,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceModifiedByUserUuid uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.VERSION,
+ selected: false,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid) => <ResourceVersion uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.CREATED_AT,
+ selected: false,
+ configurable: true,
+ sort: { direction: SortDirection.NONE, field: 'createdAt' },
+ filters: createTree(),
+ render: (uuid) => <ResourceCreatedAtDate uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.LAST_MODIFIED,
+ selected: true,
+ configurable: true,
+ sort: { direction: SortDirection.DESC, field: 'modifiedAt' },
+ filters: createTree(),
+ render: (uuid) => <ResourceLastModifiedDate uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.TRASH_AT,
+ selected: false,
+ configurable: true,
+ sort: { direction: SortDirection.NONE, field: 'trashAt' },
+ filters: createTree(),
+ render: (uuid) => <ResourceTrashDate uuid={uuid} />,
+ },
+ {
+ name: SharedWithMePanelColumnNames.DELETE_AT,
+ selected: false,
+ configurable: true,
+ sort: { direction: SortDirection.NONE, field: 'deleteAt' },
+ filters: createTree(),
+ render: (uuid) => <ResourceDeleteDate uuid={uuid} />,
+ },
+];
+
+
interface SharedWithMePanelDataProps {
resources: ResourcesState;
userUuid: string;
import { StyleRulesCallback, Typography, WithStyles, withStyles } from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
import { ProcessResource } from 'models/process';
+import { SubprocessProgressBar } from 'components/subprocess-progress-bar/subprocess-progress-bar';
+import { Process } from 'store/processes/process';
type CssRules = 'iconHeader' | 'cardHeader';
];
export interface SubprocessPanelDataProps {
+ process: Process;
resources: ResourcesState;
}
doUnMaximizePanel={props.doUnMaximizePanel}
panelMaximized={props.panelMaximized}
panelName={props.panelName}
- title={<SubProcessesTitle/>} />;
+ title={<SubProcessesTitle/>}
+ progressBar={<SubprocessProgressBar process={props.process} />} />;
};
},
});
-const mapStateToProps = (state: RootState): SubprocessPanelDataProps => ({
+const mapStateToProps = (state: RootState): Omit<SubprocessPanelDataProps,'process'> => ({
resources: state.resources,
});
},
splitter: {
"& > .layout-splitter": {
- width: "2px",
+ width: "3px",
},
"& > .layout-splitter-disabled": {
pointerEvents: "none",
const totalWidth: number = document.getElementsByClassName("splitter-layout")[0]?.clientWidth;
const rightPanelExpandedWidth = (totalWidth - COLLAPSE_ICON_SIZE) / (totalWidth / 100);
if (rightPanel) {
- rightPanel.setAttribute("style", `width: ${isCollapsed ? rightPanelExpandedWidth : getSplitterInitialSize()}%`);
+ rightPanel.setAttribute("style", `width: ${isCollapsed ? `calc(${rightPanelExpandedWidth}% - 1rem)` : `${getSplitterInitialSize()}%`}`);
}
const splitter = document.getElementsByClassName("layout-splitter")[0];
isCollapsed ? splitter?.classList.add("layout-splitter-disabled") : splitter?.classList.remove("layout-splitter-disabled");
languageName: node
linkType: hard
+"@coreui/coreui@npm:^4.3.2":
+ version: 4.3.2
+ resolution: "@coreui/coreui@npm:4.3.2"
+ dependencies:
+ postcss-combine-duplicated-selectors: ^10.0.3
+ peerDependencies:
+ "@popperjs/core": ^2.11.6
+ checksum: 88fc70f4f681bb796e1d81ca8472a3d36bfcf92866fc7c6810ead850bc371c99bca123a94abb0fafdf2935972d130005cd62b485406631cfd9abd8f38e14be15
+ languageName: node
+ linkType: hard
+
+"@coreui/react@npm:^4.11.0":
+ version: 4.11.0
+ resolution: "@coreui/react@npm:4.11.0"
+ peerDependencies:
+ "@coreui/coreui": 4.3.0
+ react: ">=17"
+ react-dom: ">=17"
+ checksum: 75c9394125e41e24fb5855b82cba93c9abeea080f9ee5bcc063ff2e581318b85c5bbef6f2c5300f5fd7a3450743488daa29b4baee6feabec38a009a452876a88
+ languageName: node
+ linkType: hard
+
"@csstools/convert-colors@npm:^1.4.0":
version: 1.4.0
resolution: "@csstools/convert-colors@npm:1.4.0"
version: 0.0.0-use.local
resolution: "arvados-workbench-2@workspace:."
dependencies:
+ "@coreui/coreui": ^4.3.2
+ "@coreui/react": ^4.11.0
"@date-io/date-fns": 1
"@fortawesome/fontawesome-svg-core": 1.2.28
"@fortawesome/free-solid-svg-icons": 5.13.0
axios-mock-adapter: 1.17.0
babel-core: 6.26.3
babel-runtime: 6.26.0
+ bootstrap: ^5.3.2
caniuse-lite: 1.0.30001299
classnames: 2.2.6
cwlts: 1.15.29
material-ui-pickers: ^2.2.4
mem: 4.0.0
mime: ^3.0.0
- moment: 2.29.1
+ moment: ^2.29.4
node-sass: ^9.0.0
node-sass-chokidar: ^2.0.0
parse-duration: 0.4.4
prop-types: 15.7.2
query-string: 6.9.0
- react: 16.8.6
+ react: 16.14.0
react-copy-to-clipboard: 5.0.3
react-dnd: 5.0.0
react-dnd-html5-backend: 5.0.1
- react-dom: 16.8.6
+ react-dom: 16.14.0
react-dropzone: 5.1.1
react-highlight-words: 0.14.0
react-idle-timer: 4.3.6
languageName: node
linkType: hard
+"bootstrap@npm:^5.3.2":
+ version: 5.3.2
+ resolution: "bootstrap@npm:5.3.2"
+ peerDependencies:
+ "@popperjs/core": ^2.11.8
+ checksum: d5580b253d121ffc137388d41da58dce8d15f1ccd574e12f28d4a08e7649ca15e95db645b2b677cb8025bccd446bff04138fc0fe64f8cba0ccc5dc004a8644cf
+ languageName: node
+ linkType: hard
+
"brace-expansion@npm:^1.1.7":
version: 1.1.11
resolution: "brace-expansion@npm:1.1.11"
languageName: node
linkType: hard
-"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000981, caniuse-lite@npm:^1.0.30001035, caniuse-lite@npm:^1.0.30001109":
- version: 1.0.30001486
- resolution: "caniuse-lite@npm:1.0.30001486"
- checksum: 5e8c2ba2679e4ad17dea6d2761a6449b814441bfeac81af6cc9d58af187df6af4b79b27befcbfc4a557e720b21c0399a7d1911c8705922e38938dcc0f40b5d4b
- languageName: node
- linkType: hard
-
-"caniuse-lite@npm:^1.0.30001541":
- version: 1.0.30001543
- resolution: "caniuse-lite@npm:1.0.30001543"
- checksum: 1a65c8b0b93913b6241c7d66e1e1f3ea0f194f7e140eefe500512641c2eb4df285991ec9869a1ba2856ea6f6d21e9f3d7bcd91971b5fb1721e3fa0390feec6f1
+"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000981, caniuse-lite@npm:^1.0.30001035, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001541":
+ version: 1.0.30001561
+ resolution: "caniuse-lite@npm:1.0.30001561"
+ checksum: 949829fe037e23346595614e01d362130245920503a12677f2506ce68e1240360113d6383febed41e8aa38cd0f5fd9c69c21b0af65a71c0246d560db489f1373
languageName: node
linkType: hard
languageName: node
linkType: hard
-"moment@npm:2.29.1":
- version: 2.29.1
- resolution: "moment@npm:2.29.1"
- checksum: 1e14d5f422a2687996be11dd2d50c8de3bd577c4a4ca79ba5d02c397242a933e5b941655de6c8cb90ac18f01cc4127e55b4a12ae3c527a6c0a274e455979345e
- languageName: node
- linkType: hard
-
-"moment@npm:^2.27.0":
+"moment@npm:^2.27.0, moment@npm:^2.29.4":
version: 2.29.4
resolution: "moment@npm:2.29.4"
checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e
languageName: node
linkType: hard
+"postcss-combine-duplicated-selectors@npm:^10.0.3":
+ version: 10.0.3
+ resolution: "postcss-combine-duplicated-selectors@npm:10.0.3"
+ dependencies:
+ postcss-selector-parser: ^6.0.4
+ peerDependencies:
+ postcss: ^8.1.0
+ checksum: 45c3dff41d0cddb510752ed92fe8c7fc66e5cf88f4988314655419d3ecdf1dc66f484a25ee73f4f292da5da851a0fdba0ec4d59bdedeee935d05b26d31d997ed
+ languageName: node
+ linkType: hard
+
"postcss-convert-values@npm:^4.0.1":
version: 4.0.1
resolution: "postcss-convert-values@npm:4.0.1"
languageName: node
linkType: hard
+"postcss-selector-parser@npm:^6.0.4":
+ version: 6.0.13
+ resolution: "postcss-selector-parser@npm:6.0.13"
+ dependencies:
+ cssesc: ^3.0.0
+ util-deprecate: ^1.0.2
+ checksum: f89163338a1ce3b8ece8e9055cd5a3165e79a15e1c408e18de5ad8f87796b61ec2d48a2902d179ae0c4b5de10fccd3a325a4e660596549b040bc5ad1b465f096
+ languageName: node
+ linkType: hard
+
"postcss-svgo@npm:^4.0.3":
version: 4.0.3
resolution: "postcss-svgo@npm:4.0.3"
languageName: node
linkType: hard
-"react-dom@npm:16.8.6":
- version: 16.8.6
- resolution: "react-dom@npm:16.8.6"
+"react-dom@npm:16.14.0":
+ version: 16.14.0
+ resolution: "react-dom@npm:16.14.0"
dependencies:
loose-envify: ^1.1.0
object-assign: ^4.1.1
prop-types: ^15.6.2
- scheduler: ^0.13.6
+ scheduler: ^0.19.1
peerDependencies:
- react: ^16.0.0
- checksum: 7f8ebd8523eb4a14a1439efa009d020abc0529da25d0de251a4f3d5b3781061f6b30d72425f5fe944317850997efc6c1d667e99b1fd70172f30a976a00008bf6
+ react: ^16.14.0
+ checksum: 5a5c49da0f106b2655a69f96c622c347febcd10532db391c262b26aec225b235357d9da1834103457683482ab1b229af7a50f6927a6b70e53150275e31785544
languageName: node
linkType: hard
languageName: node
linkType: hard
-"react@npm:16.8.6":
- version: 16.8.6
- resolution: "react@npm:16.8.6"
+"react@npm:16.14.0":
+ version: 16.14.0
+ resolution: "react@npm:16.14.0"
dependencies:
loose-envify: ^1.1.0
object-assign: ^4.1.1
prop-types: ^15.6.2
- scheduler: ^0.13.6
- checksum: 8dfdbec9af6999c2cfb33a9389995c6401daba732e1ee7e0a4920d28fd2e8e6b0fde99dfe4b8e2f81efc4a962c92656e3e79e221323449e55850232163f15ff4
+ checksum: 8484f3ecb13414526f2a7412190575fc134da785c02695eb92bb6028c930bfe1c238d7be2a125088fec663cc7cda0a3623373c46807cf2c281f49c34b79881ac
languageName: node
linkType: hard
languageName: node
linkType: hard
-"scheduler@npm:^0.13.6":
- version: 0.13.6
- resolution: "scheduler@npm:0.13.6"
- dependencies:
- loose-envify: ^1.1.0
- object-assign: ^4.1.1
- checksum: c82c705f6d0d6df87b26bf2cca33f427e91889438c0435ade3ee7f41860eda4dd7f3171ca2d93e8fe9431f3bd831ca0e267a401a0296e4b14de05e389f82d320
- languageName: node
- linkType: hard
-
"scheduler@npm:^0.19.1":
version: 0.19.1
resolution: "scheduler@npm:0.19.1"