.env.development.local
.env.test.local
.env.production.local
+.npm.local
npm-debug.log*
yarn-debug.log*
.licenseignore
.yarnrc
.npmrc
+src/lib/cwl-svg/*
}
```
+#### API_HOST
+
+The Arvados base URL.
+
+The `REACT_APP_ARVADOS_API_HOST` environment variable can be used to set the default URL if the run time configuration is unreachable.
+
#### VOCABULARY_URL
Local path, or any URL that allows cross-origin requests. See
[Vocabulary JSON file example](public/vocabulary-example.json).
+To use the URL defined in the Arvados cluster configuration, remove the entire `VOCABULARY_URL` entry from the runtime configuration. Found in `/config.json` by default.
+
### FILE_VIEWERS_CONFIG_URL
Local path, or any URL that allows cross-origin requests. See:
[File viewers config scheme](src/models/file-viewers-config.ts)
+To use the URL defined in the Arvados cluster configuration, remove the entire `FILE_VIEWERS_CONFIG_URL` entry from the runtime configuration. Found in `/config.json` by default.
+
### Licensing
Arvados is Free Software. See COPYING for information about Arvados Free
"@types/redux-form": "7.4.12",
"@types/reselect": "2.2.0",
"@types/shell-quote": "1.6.0",
- "axios": "0.18.0",
+ "axios": "0.18.1",
"classnames": "2.2.6",
"cwlts": "1.15.29",
"debounce": "1.2.0",
"file-saver": "2.0.1",
+ "fstream": "1.0.12",
+ "handlebars": "4.0.14",
"is-image": "2.0.0",
- "js-yaml": "3.12.0",
+ "js-yaml": "3.13.1",
"jssha": "2.3.1",
"jszip": "3.1.5",
- "lodash": "4.17.11",
+ "lodash": "4.17.13",
+ "lodash-es": "4.17.14",
+ "lodash.mergewith": "4.6.2",
+ "lodash.template": "4.5.0",
+ "mem": "4.0.0",
"react": "16.5.2",
"react-copy-to-clipboard": "5.0.1",
"react-dnd": "5.0.0",
"redux-form": "7.4.2",
"redux-thunk": "2.3.0",
"reselect": "4.0.0",
+ "set-value": "2.0.1",
"shell-quote": "1.6.1",
+ "sinon": "7.3",
+ "ts-mock-imports": "1.2.6",
+ "tslint-etc": "1.6.0",
"unionize": "2.1.2",
- "uuid": "3.3.2"
+ "uuid": "3.3.2",
+ "webpack-dev-server": "3.1.1"
},
"scripts": {
"start": "react-scripts-ts start",
"@types/react-router-dom": "4.3.1",
"@types/react-router-redux": "5.0.16",
"@types/redux-devtools": "3.0.44",
+ "@types/sinon": "7.5",
"@types/uuid": "3.4.4",
"axios-mock-adapter": "1.15.0",
"enzyme": "3.6.0",
{
- "strict": false,
+ "strict_tags": false,
"tags": {
- "fruit": {
- "values": ["pineapple", "tomato", "orange", "banana", "advocado", "lemon", "apple", "peach", "strawberry"],
- "strict": true
+ "IDTAGFRUITS": {
+ "strict": false,
+ "labels": [
+ {"label": "Fruit"}
+ ],
+ "values": {
+ "IDVALFRUITS1": {
+ "labels": [
+ {"label": "Pineapple"}
+ ]
+ },
+ "IDVALFRUITS2": {
+ "labels": [
+ {"label": "Tomato"}
+ ]
+ },
+ "IDVALFRUITS3": {
+ "labels": [
+ {"label": "Orange"}
+ ]
+ },
+ "IDVALFRUITS4": {
+ "labels": [
+ {"label": "Banana"}
+ ]
+ },
+ "IDVALFRUITS5": {
+ "labels": [
+ {"label": "Advocado"}
+ ]
+ },
+ "IDVALFRUITS6": {
+ "labels": [
+ {"label": "Lemon"}
+ ]
+ },
+ "IDVALFRUITS7": {
+ "labels": [
+ {"label": "Apple"}
+ ]
+ },
+ "IDVALFRUITS8": {
+ "labels": [
+ {"label": "Peach"}
+ ]
+ },
+ "IDVALFRUITS9": {
+ "labels": [
+ {"label": "Strawberry"}
+ ]
+ }
+ }
},
- "animal": {
- "values": ["human", "dog", "elephant", "eagle"],
- "strict": false
+ "IDTAGANIMALS": {
+ "strict": false,
+ "labels": [
+ {"label": "Animal" },
+ {"label": "Creature"}
+ ],
+ "values": {
+ "IDVALANIMALS1": {
+ "labels": [
+ {"label": "Human"},
+ {"label": "Homo sapiens"}
+ ]
+ },
+ "IDVALANIMALS2": {
+ "labels": [
+ {"label": "Dog"},
+ {"label": "Canis lupus familiaris"}
+ ]
+ },
+ "IDVALANIMALS3": {
+ "labels": [
+ {"label": "Elephant"},
+ {"label": "Loxodonta"}
+ ]
+ },
+ "IDVALANIMALS4": {
+ "labels": [
+ {"label": "Eagle"},
+ {"label": "Haliaeetus leucocephalus"}
+ ]
+ }
+ }
},
- "color": {
- "values": ["yellow", "red", "magenta", "green"],
- "strict": false
+ "IDTAGCOLORS": {
+ "strict": false,
+ "labels": [
+ {"label": "Color"}
+ ],
+ "values": {
+ "IDVALCOLORS1": {
+ "labels": [
+ {"label": "Yellow"}
+ ]
+ },
+ "IDVALCOLORS2": {
+ "labels": [
+ {"label": "Red"}
+ ]
+ },
+ "IDVALCOLORS3": {
+ "labels": [
+ {"label": "Magenta"}
+ ]
+ },
+ "IDVALCOLORS4": {
+ "labels": [
+ {"label": "Green"}
+ ]
+ }
+ }
},
- "text": {},
- "category": {
- "values": ["experimental", "development", "production"]
+ "IDTAGCOMMENT": {
+ "labels": [
+ {"label": "Comment"},
+ {"label": "Text"}
+ ]
},
- "comments": {},
- "importance": {
- "values": ["critical", "important", "low priority"]
+ "IDTAGCATEGORIES": {
+ "strict": true,
+ "labels": [
+ {"label": "Category"}
+ ],
+ "values": {
+ "IDTAGCAT1": {
+ "labels": [
+ {"label": "Experimental"}
+ ]
+ },
+ "IDTAGCAT2": {
+ "labels": [
+ {"label": "Development"}
+ ]
+ },
+ "IDTAGCAT3": {
+ "labels": [
+ {"label": "Production"}
+ ]
+ }
+ }
},
- "size": {
- "values": ["x-small", "small", "medium", "large", "x-large"]
+ "IDTAGIMPORTANCES": {
+ "strict": true,
+ "labels": [
+ {"label": "Importance"},
+ {"label": "Priority"}
+ ],
+ "values": {
+ "IDVALIMPORTANCES1": {
+ "labels": [
+ {"label": "Critical"},
+ {"label": "Urgent"},
+ {"label": "High"}
+ ]
+ },
+ "IDVALIMPORTANCES2": {
+ "labels": [
+ {"label": "Normal"},
+ {"label": "Moderate"}
+ ]
+ },
+ "IDVALIMPORTANCES3": {
+ "labels": [
+ {"label": "Low"}
+ ]
+ }
+ }
},
- "country": {
- "values": ["Afghanistan","Ã…land Islands","Albania","Algeria","American Samoa","AndorrA","Angola","Anguilla","Antarctica","Antigua and Barbuda","Argentina","Armenia","Aruba","Australia","Austria","Azerbaijan","Bahamas","Bahrain","Bangladesh","Barbados","Belarus","Belgium","Belize","Benin","Bermuda","Bhutan","Bolivia","Bosnia and Herzegovina","Botswana","Bouvet Island","Brazil","British Indian Ocean Territory","Brunei Darussalam","Bulgaria","Burkina Faso","Burundi","Cambodia","Cameroon","Canada","Cape Verde","Cayman Islands","Central African Republic","Chad","Chile","China","Christmas Island","Cocos (Keeling) Islands","Colombia","Comoros","Congo","Congo, The Democratic Republic of the","Cook Islands","Costa Rica","Cote D'Ivoire","Croatia","Cuba","Cyprus","Czech Republic","Denmark","Djibouti","Dominica","Dominican Republic","Ecuador","Egypt","El Salvador","Equatorial Guinea","Eritrea","Estonia","Ethiopia","Falkland Islands (Malvinas)","Faroe Islands","Fiji","Finland","France","French Guiana","French Polynesia","French Southern Territories","Gabon","Gambia","Georgia","Germany","Ghana","Gibraltar","Greece","Greenland","Grenada","Guadeloupe","Guam","Guatemala","Guernsey","Guinea","Guinea-Bissau","Guyana","Haiti","Heard Island and Mcdonald Islands","Holy See (Vatican City State)","Honduras","Hong Kong","Hungary","Iceland","India","Indonesia","Iran, Islamic Republic Of","Iraq","Ireland","Isle of Man","Israel","Italy","Jamaica","Japan","Jersey","Jordan","Kazakhstan","Kenya","Kiribati","Korea, Democratic People'S Republic of","Korea, Republic of","Kuwait","Kyrgyzstan","Lao People'S Democratic Republic","Latvia","Lebanon","Lesotho","Liberia","Libyan Arab Jamahiriya","Liechtenstein","Lithuania","Luxembourg","Macao","Macedonia, The Former Yugoslav Republic of","Madagascar","Malawi","Malaysia","Maldives","Mali","Malta","Marshall Islands","Martinique","Mauritania","Mauritius","Mayotte","Mexico","Micronesia, Federated States of","Moldova, Republic of","Monaco","Mongolia","Montserrat","Morocco","Mozambique","Myanmar","Namibia","Nauru","Nepal","Netherlands","Netherlands Antilles","New Caledonia","New Zealand","Nicaragua","Niger","Nigeria","Niue","Norfolk Island","Northern Mariana Islands","Norway","Oman","Pakistan","Palau","Palestinian Territory, Occupied","Panama","Papua New Guinea","Paraguay","Peru","Philippines","Pitcairn","Poland","Portugal","Puerto Rico","Qatar","Reunion","Romania","Russian Federation","RWANDA","Saint Helena","Saint Kitts and Nevis","Saint Lucia","Saint Pierre and Miquelon","Saint Vincent and the Grenadines","Samoa","San Marino","Sao Tome and Principe","Saudi Arabia","Senegal","Serbia and Montenegro","Seychelles","Sierra Leone","Singapore","Slovakia","Slovenia","Solomon Islands","Somalia","South Africa","South Georgia and the South Sandwich Islands","Spain","Sri Lanka","Sudan","Suriname","Svalbard and Jan Mayen","Swaziland","Sweden","Switzerland","Syrian Arab Republic","Taiwan, Province of China","Tajikistan","Tanzania, United Republic of","Thailand","Timor-Leste","Togo","Tokelau","Tonga","Trinidad and Tobago","Tunisia","Turkey","Turkmenistan","Turks and Caicos Islands","Tuvalu","Uganda","Ukraine","United Arab Emirates","United Kingdom","United States","United States Minor Outlying Islands","Uruguay","Uzbekistan","Vanuatu","Venezuela","Viet Nam","Virgin Islands, British","Virgin Islands, U.S.","Wallis and Futuna","Western Sahara","Yemen","Zambia","Zimbabwe"],
- "strict": true
+ "IDTAGSIZES": {
+ "strict": true,
+ "labels": [
+ {"label": "Size"}
+ ],
+ "values": {
+ "IDVALSIZES1": {
+ "labels": [
+ {"label": "XS"},
+ {"label": "x-small"}
+ ]
+ },
+ "IDVALSIZES2": {
+ "labels": [
+ {"label": "S"},
+ {"label": "small"}
+ ]
+ },
+ "IDVALSIZES3": {
+ "labels": [
+ {"label": "M"},
+ {"label": "medium"}
+ ]
+ },
+ "IDVALSIZES4": {
+ "labels": [
+ {"label": "L"},
+ {"label": "large"}
+ ]
+ },
+ "IDVALSIZES5": {
+ "labels": [
+ {"label": "XL"},
+ {"label": "x-large"}
+ ]
+ }
+ }
}
}
}
\ No newline at end of file
import Axios from "axios";
-export const CONFIG_URL = process.env.REACT_APP_ARVADOS_CONFIG_URL || "/config.json";
+export const WORKBENCH_CONFIG_URL = process.env.REACT_APP_ARVADOS_CONFIG_URL || "/config.json";
-export interface Config {
- auth: {};
- basePath: string;
+interface WorkbenchConfig {
+ API_HOST: string;
+ VOCABULARY_URL?: string;
+ FILE_VIEWERS_CONFIG_URL?: string;
+}
+
+export interface ClusterConfigJSON {
+ ClusterID: string;
+ RemoteClusters: {
+ [key: string]: {
+ ActivateUsers: boolean
+ Host: string
+ Insecure: boolean
+ Proxy: boolean
+ Scheme: string
+ }
+ };
+ Services: {
+ Controller: {
+ ExternalURL: string
+ }
+ Workbench1: {
+ ExternalURL: string
+ }
+ Workbench2: {
+ ExternalURL: string
+ }
+ Websocket: {
+ ExternalURL: string
+ }
+ WebDAV: {
+ ExternalURL: string
+ }
+ };
+ Workbench: {
+ ArvadosDocsite: string;
+ VocabularyURL: string;
+ FileViewersConfigURL: string;
+ WelcomePageHTML: string;
+ InactivePageHTML: string;
+ SiteName: string;
+ };
+ Login: {
+ LoginCluster: string;
+ };
+}
+
+export class Config {
baseUrl: string;
- batchPath: string;
- blobSignatureTtl: number;
- crunchLimitLogBytesPerJob: number;
- crunchLogBytesPerEvent: number;
- crunchLogPartialLineThrottlePeriod: number;
- crunchLogSecondsBetweenEvents: number;
- crunchLogThrottleBytes: number;
- crunchLogThrottleLines: number;
- crunchLogThrottlePeriod: number;
- defaultCollectionReplication: number;
- defaultTrashLifetime: number;
- description: string;
- discoveryVersion: string;
- dockerImageFormats: string[];
- documentationLink: string;
- generatedAt: string;
- gitUrl: string;
- id: string;
keepWebServiceUrl: string;
- kind: string;
- maxRequestSize: number;
- name: string;
- packageVersion: string;
- parameters: {};
- protocol: string;
remoteHosts: {
[key: string]: string
};
- remoteHostsViaDNS: boolean;
- resources: {};
- revision: string;
rootUrl: string;
- schemas: {};
- servicePath: string;
- sourceVersion: string;
- source_version: string;
- title: string;
uuidPrefix: string;
- version: string;
websocketUrl: string;
workbenchUrl: string;
- workbench2Url?: string;
+ workbench2Url: string;
vocabularyUrl: string;
fileViewersConfigUrl: string;
+ loginCluster: string;
+ clusterConfig: ClusterConfigJSON;
}
+export const buildConfig = (clusterConfigJSON: ClusterConfigJSON): Config => {
+ const config = new Config();
+ config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
+ config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
+ config.uuidPrefix = clusterConfigJSON.ClusterID;
+ config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
+ config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
+ config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
+ config.keepWebServiceUrl = clusterConfigJSON.Services.WebDAV.ExternalURL;
+ config.loginCluster = clusterConfigJSON.Login.LoginCluster;
+ config.clusterConfig = clusterConfigJSON;
+ mapRemoteHosts(clusterConfigJSON, config);
+ return config;
+};
+
export const fetchConfig = () => {
return Axios
- .get<ConfigJSON>(CONFIG_URL + "?nocache=" + (new Date()).getTime())
+ .get<WorkbenchConfig>(WORKBENCH_CONFIG_URL + "?nocache=" + (new Date()).getTime())
.then(response => response.data)
- .catch(() => Promise.resolve(getDefaultConfig()))
- .then(config => Axios
- .get<Config>(getDiscoveryURL(config.API_HOST))
- .then(response => ({
- // TODO: After tests delete `|| '/vocabulary-example.json'`
- // TODO: After tests delete `|| '/file-viewers-example.json'`
- config: {
- ...response.data,
- vocabularyUrl: config.VOCABULARY_URL || '/vocabulary-example.json',
- fileViewersConfigUrl: config.FILE_VIEWERS_CONFIG_URL || '/file-viewers-example.json'
- },
- apiHost: config.API_HOST,
- })));
+ .catch(() => {
+ console.warn(`There was an exception getting the Workbench config file at ${WORKBENCH_CONFIG_URL}. Using defaults instead.`);
+ return Promise.resolve(getDefaultConfig());
+ })
+ .then(workbenchConfig => {
+ if (workbenchConfig.API_HOST === undefined) {
+ throw new Error(`Unable to start Workbench. API_HOST is undefined in ${WORKBENCH_CONFIG_URL} or the environment.`);
+ }
+ return Axios.get<ClusterConfigJSON>(getClusterConfigURL(workbenchConfig.API_HOST)).then(response => {
+ const clusterConfigJSON = response.data;
+ const config = buildConfig(clusterConfigJSON);
+ const warnLocalConfig = (varName: string) => console.warn(
+ `A value for ${varName} was found in ${WORKBENCH_CONFIG_URL}. To use the Arvados centralized configuration instead, \
+remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`);
+
+ // Check if the workbench config has an entry for vocabulary and file viewer URLs
+ // If so, use these values (even if it is an empty string), but print a console warning.
+ // Otherwise, use the cluster config.
+ let fileViewerConfigUrl;
+ if (workbenchConfig.FILE_VIEWERS_CONFIG_URL !== undefined) {
+ warnLocalConfig("FILE_VIEWERS_CONFIG_URL");
+ fileViewerConfigUrl = workbenchConfig.FILE_VIEWERS_CONFIG_URL;
+ }
+ else {
+ fileViewerConfigUrl = clusterConfigJSON.Workbench.FileViewersConfigURL || "/file-viewers-example.json";
+ }
+ config.fileViewersConfigUrl = fileViewerConfigUrl;
+ let vocabularyUrl;
+ if (workbenchConfig.VOCABULARY_URL !== undefined) {
+ warnLocalConfig("VOCABULARY_URL");
+ vocabularyUrl = workbenchConfig.VOCABULARY_URL;
+ }
+ else {
+ vocabularyUrl = clusterConfigJSON.Workbench.VocabularyURL || "/vocabulary-example.json";
+ }
+ config.vocabularyUrl = vocabularyUrl;
+
+ return { config, apiHost: workbenchConfig.API_HOST };
+ });
+ });
+};
+
+// Maps remote cluster hosts and removes the default RemoteCluster entry
+export const mapRemoteHosts = (clusterConfigJSON: ClusterConfigJSON, config: Config) => {
+ config.remoteHosts = {};
+ Object.keys(clusterConfigJSON.RemoteClusters).forEach(k => { config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host; });
+ delete config.remoteHosts["*"];
};
+export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): ClusterConfigJSON => ({
+ ClusterID: "",
+ RemoteClusters: {},
+ Services: {
+ Controller: { ExternalURL: "" },
+ Workbench1: { ExternalURL: "" },
+ Workbench2: { ExternalURL: "" },
+ Websocket: { ExternalURL: "" },
+ WebDAV: { ExternalURL: "" },
+ },
+ Workbench: {
+ ArvadosDocsite: "",
+ VocabularyURL: "",
+ FileViewersConfigURL: "",
+ WelcomePageHTML: "",
+ InactivePageHTML: "",
+ SiteName: "",
+ },
+ Login: {
+ LoginCluster: "",
+ },
+ ...config
+});
+
export const mockConfig = (config: Partial<Config>): Config => ({
- auth: {},
- basePath: '',
- baseUrl: '',
- batchPath: '',
- blobSignatureTtl: 0,
- crunchLimitLogBytesPerJob: 0,
- crunchLogBytesPerEvent: 0,
- crunchLogPartialLineThrottlePeriod: 0,
- crunchLogSecondsBetweenEvents: 0,
- crunchLogThrottleBytes: 0,
- crunchLogThrottleLines: 0,
- crunchLogThrottlePeriod: 0,
- defaultCollectionReplication: 0,
- defaultTrashLifetime: 0,
- description: '',
- discoveryVersion: '',
- dockerImageFormats: [],
- documentationLink: '',
- generatedAt: '',
- gitUrl: '',
- id: '',
- keepWebServiceUrl: '',
- kind: '',
- maxRequestSize: 0,
- name: '',
- packageVersion: '',
- parameters: {},
- protocol: '',
+ baseUrl: "",
+ keepWebServiceUrl: "",
remoteHosts: {},
- remoteHostsViaDNS: false,
- resources: {},
- revision: '',
- rootUrl: '',
- schemas: {},
- servicePath: '',
- sourceVersion: '',
- source_version: '',
- title: '',
- uuidPrefix: '',
- version: '',
- websocketUrl: '',
- workbenchUrl: '',
- vocabularyUrl: '',
- fileViewersConfigUrl: '',
+ rootUrl: "",
+ uuidPrefix: "",
+ websocketUrl: "",
+ workbenchUrl: "",
+ workbench2Url: "",
+ vocabularyUrl: "",
+ fileViewersConfigUrl: "",
+ loginCluster: "",
+ clusterConfig: mockClusterConfigJSON({}),
...config
});
-interface ConfigJSON {
- API_HOST: string;
- VOCABULARY_URL: string;
- FILE_VIEWERS_CONFIG_URL: string;
-}
-
-const getDefaultConfig = (): ConfigJSON => ({
- API_HOST: process.env.REACT_APP_ARVADOS_API_HOST || "",
- VOCABULARY_URL: "",
- FILE_VIEWERS_CONFIG_URL: "",
-});
+const getDefaultConfig = (): WorkbenchConfig => {
+ let apiHost = "";
+ const envHost = process.env.REACT_APP_ARVADOS_API_HOST;
+ if (envHost !== undefined) {
+ console.warn(`Using default API host ${envHost}.`);
+ apiHost = envHost;
+ }
+ else {
+ console.warn(`No API host was found in the environment. Workbench may not be able to communicate with Arvados components.`);
+ }
+ return {
+ API_HOST: apiHost,
+ VOCABULARY_URL: undefined,
+ FILE_VIEWERS_CONFIG_URL: undefined,
+ };
+};
-export const DISCOVERY_URL = 'discovery/v1/apis/arvados/v1/rest';
-export const getDiscoveryURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${DISCOVERY_URL}?nocache=${(new Date()).getTime()}`;
+export const ARVADOS_API_PATH = "arvados/v1";
+export const CLUSTER_CONFIG_PATH = "arvados/v1/config";
+export const DISCOVERY_DOC_PATH = "discovery/v1/apis/arvados/v1/rest";
+export const getClusterConfigURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${(new Date()).getTime()}`;
// SPDX-License-Identifier: AGPL-3.0
import { PropertyValue } from "~/models/search-bar";
+import { Vocabulary, getTagKeyLabel, getTagValueLabel } from "~/models/vocabulary";
export const formatDate = (isoDate?: string | null, utc: boolean = false) => {
if (isoDate) {
}
];
-export const formatPropertyValue = (pv: PropertyValue) => {
+export const formatPropertyValue = (pv: PropertyValue, vocabulary?: Vocabulary) => {
+ if (vocabulary && pv.keyID && pv.valueID) {
+ return `${getTagKeyLabel(pv.keyID, vocabulary)}: ${getTagValueLabel(pv.keyID, pv.valueID!, vocabulary)}`;
+ }
if (pv.key) {
return pv.value
? `${pv.key}: ${pv.value}`
--- /dev/null
+import { RootState } from '~/store/store';
+
+export const getUserUuid = (state: RootState) => {
+ const user = state.auth.user;
+ if (user) {
+ return user.uuid;
+ } else {
+ return undefined;
+ }
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const escapeRegExp = (st: string) =>
+ st.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
const results = regex.exec(search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
+
+export function normalizeURLPath(url: string) {
+ const u = new URL(url);
+ u.pathname = u.pathname.replace(/\/\//, '/');
+ if (u.pathname[u.pathname.length - 1] === '/') {
+ u.pathname = u.pathname.substr(0, u.pathname.length - 1);
+ }
+ return u.toString();
+}
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Input as MuiInput, Chip as MuiChip, Popper as MuiPopper, Paper as MuiPaper, FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, ListItemText, ListItem, List, FormHelperText } from '@material-ui/core';
+import {
+ Input as MuiInput,
+ Chip as MuiChip,
+ Popper as MuiPopper,
+ Paper as MuiPaper,
+ FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, ListItemText, ListItem, List, FormHelperText
+} from '@material-ui/core';
import { PopperProps } from '@material-ui/core/Popper';
import { WithStyles } from '@material-ui/core/styles';
import { noop } from 'lodash';
import { Button, Grid, StyleRulesCallback, WithStyles, Typography, Tooltip } from '@material-ui/core';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import { withStyles } from '@material-ui/core';
+import { IllegalNamingWarning } from '../warning/warning';
export interface Breadcrumb {
label: string;
{
items.map((item, index) => {
const isLastItem = index === items.length - 1;
+ const isFirstItem = index === 0;
return (
<React.Fragment key={index}>
+ {isFirstItem ? null : <IllegalNamingWarning name={item.label} />}
<Tooltip title={item.label}>
<Button
color="inherit"
import { formatFileSize } from "~/common/formatters";
import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon";
import { FileTreeData } from "./file-tree-data";
-import { FileThumbnail } from '~/components/file-tree/file-thumbnail';
type CssRules = "root" | "spacer" | "sizeInfo" | "button" | "moreOptions";
<Dialog
open={props.open}
onClose={props.closeDialog}
- disableBackdropClick={props.submitting}
+ disableBackdropClick
disableEscapeKeyDown={props.submitting}
fullWidth
maxWidth='md'>
import Delete from '@material-ui/icons/Delete';
import DeviceHub from '@material-ui/icons/DeviceHub';
import Edit from '@material-ui/icons/Edit';
+import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import Folder from '@material-ui/icons/Folder';
import GetApp from '@material-ui/icons/GetApp';
export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
export const EditSavedQueryIcon: IconType = (props) => <Create {...props} />;
export const ExpandIcon: IconType = (props) => <ExpandMoreIcon {...props} />;
+export const ErrorIcon: IconType = (props) => <ErrorRoundedIcon style={{color: '#ff0000'}} {...props} />;
export const FavoriteIcon: IconType = (props) => <Star {...props} />;
export const HelpIcon: IconType = (props) => <Help {...props} />;
export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
isActive?: boolean;
hasMargin?: boolean;
iconSize?: number;
+ nameDecorator?: JSX.Element;
}
type ListItemTextIconProps = ListItemTextIconDataProps & WithStyles<CssRules>;
export const ListItemTextIcon = withStyles(styles)(
class extends React.Component<ListItemTextIconProps, {}> {
render() {
- const { classes, isActive, hasMargin, name, icon: Icon, iconSize } = this.props;
+ const { classes, isActive, hasMargin, name, icon: Icon, iconSize, nameDecorator } = this.props;
return (
<Typography component='span' className={classes.root}>
<ListItemIcon className={classnames({
<Icon style={{ fontSize: `${iconSize}rem` }} />
</ListItemIcon>
+ {nameDecorator || null}
<ListItemText primary={
- <Typography className={classnames(classes.listItemText, {
+ <Typography className={classnames(classes.listItemText, {
[classes.active]: isActive
})}>
{name}
timeout: number;
render() {
- const { classes } = this.props;
return <form onSubmit={this.handleSubmit}>
<FormControl>
<InputLabel>Search</InputLabel>
import * as React from 'react';
import { WrappedFieldProps } from 'redux-form';
import { ArvadosTheme } from '~/common/custom-theme';
-import { StyleRulesCallback, WithStyles, withStyles, FormControl, InputLabel, Select, MenuItem, FormHelperText } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, withStyles, FormControl, InputLabel, Select, FormHelperText } from '@material-ui/core';
type CssRules = 'formControl' | 'selectWrapper' | 'select' | 'option';
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { ErrorIcon } from "~/components/icon/icon";
+import { invalidNamingRules } from "~/validators/valid-name";
+import { Tooltip } from "@material-ui/core";
+
+interface WarningComponentProps {
+ text: string;
+ rules: RegExp[];
+ message: string;
+}
+
+export const WarningComponent = ({ text, rules, message }: WarningComponentProps) =>
+ rules.find(aRule => text.match(aRule) !== null)
+ ? message
+ ? <Tooltip title={message}><ErrorIcon /></Tooltip>
+ : <ErrorIcon />
+ : null;
+
+interface IllegalNamingWarningProps {
+ name: string;
+}
+
+export const IllegalNamingWarning = ({ name }: IllegalNamingWarningProps) =>
+ <WarningComponent
+ text={name} rules={invalidNamingRules}
+ message="Names being '.', '..' or including '/' cause issues with WebDAV, please edit it to something different." />;
\ No newline at end of file
import { configureStore, RootStore } from '~/store/store';
import { ConnectedRouter } from "react-router-redux";
import { ApiToken } from "~/views-components/api-token/api-token";
+import { AddSession } from "~/views-components/add-session/add-session";
import { initAuth } from "~/store/auth/auth-action";
import { createServices } from "~/services/services";
import { MuiThemeProvider } from '@material-ui/core/styles';
import { processResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
import { trashedCollectionActionSet } from '~/views-components/context-menu/action-sets/trashed-collection-action-set';
-import { ContainerRequestState } from '~/models/container-request';
-import { MountKind } from '~/models/mount-types';
import { setBuildInfo } from '~/store/app-info/app-info-actions';
import { getBuildInfo } from '~/common/app-info';
import { DragDropContextProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
-import { initAdvanceFormProjectsTree } from '~/store/search-bar/search-bar-actions';
+import { initAdvancedFormProjectsTree } from '~/store/search-bar/search-bar-actions';
import { repositoryActionSet } from '~/views-components/context-menu/action-sets/repository-action-set';
import { sshKeyActionSet } from '~/views-components/context-menu/action-sets/ssh-key-action-set';
import { keepServiceActionSet } from '~/views-components/context-menu/action-sets/keep-service-action-set';
import { collectionAdminActionSet } from '~/views-components/context-menu/action-sets/collection-admin-action-set';
import { processResourceAdminActionSet } from '~/views-components/context-menu/action-sets/process-resource-admin-action-set';
import { projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
console.log(`Starting arvados [${getBuildInfo()}]`);
store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
},
errorFn: (id, error) => {
- // console.error("Backend error:", error);
- // store.dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Backend error", kind: SnackbarKind.ERROR }));
+ console.error("Backend error:", error);
+ if (error.errors) {
+ store.dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: `${error.errors[0]}`,
+ kind: SnackbarKind.ERROR,
+ hideDuration: 8000
+ }));
+ } else {
+ store.dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: `${error.message}`,
+ kind: SnackbarKind.ERROR,
+ hideDuration: 8000
+ }));
+ }
}
});
const store = configureStore(history, services);
store.dispatch(loadFileViewersConfig);
const TokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={true} {...props} />;
+ const AddSessionComponent = (props: any) => <AddSession {...props} />;
const FedTokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={false} {...props} />;
const MainPanelComponent = (props: any) => <MainPanel {...props} />;
<Switch>
<Route path={Routes.TOKEN} component={TokenComponent} />
<Route path={Routes.FED_LOGIN} component={FedTokenComponent} />
+ <Route path={Routes.ADD_SESSION} component={AddSessionComponent} />
<Route path={Routes.ROOT} component={MainPanelComponent} />
</Switch>
</ConnectedRouter>
await store.dispatch(loadWorkbench());
addRouteChangeHandlers(history, store);
// ToDo: move to searchBar component
- store.dispatch(initAdvanceFormProjectsTree());
+ store.dispatch(initAdvancedFormProjectsTree());
}
};
};
-const createDirectoriesArrayCollectorWorkflow = ({ workflowService }: ServiceRepository) => {
- workflowService.create({
- name: 'Directories array collector',
- description: 'Workflow for collecting directories array',
- definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"multiple_collections\":\n $(inputs.multiple_collections)\n\n class: InitialWorkDirRequirement\n inputs:\n - type:\n type: array\n items: Directory\n id: '#input_collector.cwl/multiple_collections'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type:\n type: array\n items: Directory\n label: Multiple Collections\n doc: This should allow for selecting multiple collections.\n id: '#main/multiple_collections'\n default:\n - class: Directory\n location: keep:1e1682585d576f031b2d8b4944f989ee+57\n basename: 1e1682585d576f031b2d8b4944f989ee+57\n - class: Directory\n location: keep:326f692370e9e121fcbd013796f7352a+57\n basename: 326f692370e9e121fcbd013796f7352a+57\n \n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/multiple_collections'\n id: '#main/input_collector/multiple_collections'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n",
- });
-};
-
-const createPrimitiveArraysCollectorWorkflow = ({ workflowService }: ServiceRepository) => {
- workflowService.create({
- name: 'String, Int and Float arrays collector',
- description: 'Workflow for collecting primitive data arrays',
- definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"string array\":\n $(inputs.example_string_array)\n \"int array\":\n $(inputs.example_int_array)\n \"float array\":\n $(inputs.example_float_array)\n\n class: InitialWorkDirRequirement\n inputs:\n - type:\n type: array\n items: string\n id: '#input_collector.cwl/example_string_array'\n - type:\n type: array\n items: int\n id: '#input_collector.cwl/example_int_array'\n - type:\n type: array\n items: float\n id: '#input_collector.cwl/example_float_array'\n \n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type:\n type: array\n items: string\n label: Freetext Array\n doc: This should allow for entering multiple strings.\n id: '#main/example_string_array'\n default:\n - This is the first string\n - This is the second string\n - type:\n type: array\n items: int\n label: Integer Array\n doc: This should allow for entering multiple integers.\n id: '#main/example_int_array'\n default:\n - 3\n - 6\n - type:\n type: array\n items: float\n label: Float Array\n doc: This should allow for entering multiple floats.\n id: '#main/example_float_array'\n default:\n - 3.33\n - 66.6\n\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/example_string_array'\n id: '#main/input_collector/example_string_array'\n - source: '#main/example_int_array'\n id: '#main/input_collector/example_int_array'\n - source: '#main/example_float_array'\n id: '#main/input_collector/example_float_array'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n",
- });
-};
-
-const createFilesArrayCollectorWorkflow = ({ workflowService }: ServiceRepository) => {
- workflowService.create({
- name: 'Files array collector',
- description: 'Workflow for collecting files array',
- definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"multiple_files\":\n $(inputs.multiple_files)\n\n class: InitialWorkDirRequirement\n inputs:\n - type:\n type: array\n items: File\n id: '#input_collector.cwl/multiple_files'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n\n baseCommand: [cat]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type:\n type: array\n items: File\n label: Multiple Files\n doc: This should allow for selecting multiple files.\n id: '#main/multiple_files'\n default:\n - class: File\n location: keep:af831660d820bcbb98f473355e6e1b85+67/fileA\n basename: fileA\n nameroot: fileA\n nameext: ''\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/multiple_files'\n id: '#main/input_collector/multiple_files'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n",
- });
-};
-
-const createPrimitivesCollectorWorkflow = ({ workflowService }: ServiceRepository) => {
- workflowService.create({
- name: 'Primitive values collector',
- description: 'Workflow for collecting primitive values',
- definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"flag\":\n $(inputs.example_flag)\n \"string\":\n $(inputs.example_string)\n \"int\":\n $(inputs.example_int)\n \"long\":\n $(inputs.example_long)\n \"float\":\n $(inputs.example_float)\n \"double\":\n $(inputs.example_double)\n class: InitialWorkDirRequirement\n inputs:\n - type: double\n id: '#input_collector.cwl/example_double'\n - type: boolean\n id: '#input_collector.cwl/example_flag'\n - type: float\n id: '#input_collector.cwl/example_float'\n - type: int\n id: '#input_collector.cwl/example_int'\n - type: long\n id: '#input_collector.cwl/example_long'\n - type: string\n id: '#input_collector.cwl/example_string'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: Workflw for collecting primitive values\n inputs:\n - type: double\n label: Double value\n doc: This should allow for entering a decimal number (64-bit).\n id: '#main/example_double'\n default: 0.3333333333333333\n - type: boolean\n label: Boolean Flag\n doc: This should render as in checkbox.\n id: '#main/example_flag'\n default: true\n - type: float\n label: Float value\n doc: This should allow for entering a decimal number (32-bit).\n id: '#main/example_float'\n default: 0.15625\n - type: int\n label: Integer Number\n doc: This should allow for entering a number (32-bit signed).\n id: '#main/example_int'\n default: 2147483647\n - type: long\n label: Long Number\n doc: This should allow for entering a number (64-bit signed).\n id: '#main/example_long'\n default: 9223372036854775807\n - type: string\n label: Freetext\n doc: This should allow for entering an arbitrary char sequence.\n id: '#main/example_string'\n default: This is a string\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/example_double'\n id: '#main/input_collector/example_double'\n - source: '#main/example_flag'\n id: '#main/input_collector/example_flag'\n - source: '#main/example_float'\n id: '#main/input_collector/example_float'\n - source: '#main/example_int'\n id: '#main/input_collector/example_int'\n - source: '#main/example_long'\n id: '#main/input_collector/example_long'\n - source: '#main/example_string'\n id: '#main/input_collector/example_string'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n",
- });
-};
-
-const createEnumCollectorWorkflow = ({ workflowService }: ServiceRepository) => {
- workflowService.create({
- name: 'Enum values collector',
- description: 'Workflow for collecting enum values',
- definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"enum_type\":\n $(inputs.enum_type)\n\n class: InitialWorkDirRequirement\n inputs:\n - type:\n type: enum\n symbols: ['#input_collector.cwl/enum_type/OTU table', '#input_collector.cwl/enum_type/Pathway\n table', '#input_collector.cwl/enum_type/Function table', '#input_collector.cwl/enum_type/Ortholog\n table']\n id: '#input_collector.cwl/enum_type'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type:\n type: enum\n symbols: ['#main/enum_type/OTU table', '#main/enum_type/Pathway table', '#main/enum_type/Function\n table', '#main/enum_type/Ortholog table']\n name: '#enum_typef4179c7f-45f9-482d-a5db-1abb86698384'\n label: Enumeration Type\n doc: This should render as a drop-down menu.\n id: '#main/enum_type'\n default: OTU table\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/enum_type'\n id: '#main/input_collector/enum_type'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n",
- });
-};
-
-const createFilesCollectorWorkflow = ({ workflowService }: ServiceRepository) => {
- workflowService.create({
- name: 'File values collector',
- description: 'Workflow for collecting file values',
- definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"single_file\":\n $(inputs.single_file.basename)\n \"optional_file\":\n $(inputs.optional_file.basename)\n\n class: InitialWorkDirRequirement\n inputs:\n - type:\n - 'null'\n - File\n id: '#input_collector.cwl/optional_file'\n - type:\n - 'null'\n - File\n id: '#input_collector.cwl/optional_file_missing_label'\n - type: File\n id: '#input_collector.cwl/single_file'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type:\n - 'null'\n - File\n label: Single File (Optional)\n doc: This should allow for single File selection only. Input should be marked\n as optional and not enforced by form validation.\n id: '#main/optional_file'\n default:\n class: File\n location: keep:af831660d820bcbb98f473355e6e1b85+67/fileA\n basename: fileA\n nameroot: fileA\n nameext: ''\n - type:\n - 'null'\n - File\n doc: Label should be the input field name because of missing label.\n id: '#main/optional_file_missing_label'\n - type: File\n label: Single File\n doc: This should allow for single File selection only.\n id: '#main/single_file'\n default:\n class: File\n location: keep:af831660d820bcbb98f473355e6e1b85+67/fileA\n basename: fileA\n nameroot: fileA\n nameext: ''\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/optional_file'\n id: '#main/input_collector/optional_file'\n - source: '#main/single_file'\n id: '#main/input_collector/single_file'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n",
- });
-};
-
-const createCollectionCollectorWorkflow = ({ workflowService }: ServiceRepository) => {
- workflowService.create({
- name: 'Collection value collector',
- description: 'Workflow for collecting a collecion',
- definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"collection\":\n $(inputs.collection.location)\n\n class: InitialWorkDirRequirement\n inputs:\n - type: Directory\n id: '#input_collector.cwl/collection'\n\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type: Directory\n label: Single Collection\n doc: This should allow for single Collection selection only.\n id: '#main/collection'\n default:\n class: Directory\n location: keep:af831660d820bcbb98f473355e6e1b85+67\n basename: af831660d820bcbb98f473355e6e1b85+67\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/collection'\n id: '#main/input_collector/collection'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n",
- });
-};
-
-const createSampleProcess = ({ containerRequestService }: ServiceRepository) => {
- containerRequestService.create({
- ownerUuid: 'c97qk-j7d0g-s3ngc1z0748hsmf',
- name: 'Simple process 7',
- state: ContainerRequestState.COMMITTED,
- mounts: {
- '/var/spool/cwl': {
- kind: MountKind.COLLECTION,
- writable: true,
- },
- 'stdout': {
- kind: MountKind.MOUNTED_FILE,
- path: '/var/spool/cwl/cwl.output.json'
- },
- '/var/lib/cwl/workflow.json': {
- kind: MountKind.JSON,
- content: {
- "cwlVersion": "v1.0",
- "$graph": [
- {
- "class": "CommandLineTool",
- "requirements": [
- {
- "listing": [
- {
- "entryname": "input_collector.log",
- "entry": "$(inputs.single_file.basename)\n"
- }
- ],
- "class": "InitialWorkDirRequirement"
- }
- ],
- "inputs": [
- {
- "type": "File",
- "id": "#input_collector.cwl/single_file"
- }
- ],
- "outputs": [
- {
- "type": "File",
- "outputBinding": {
- "glob": "*"
- },
- "id": "#input_collector.cwl/output"
- }
- ],
- "baseCommand": [
- "echo"
- ],
- "id": "#input_collector.cwl"
- },
- {
- "class": "Workflow",
- "doc": "This is the description of the workflow",
- "inputs": [
- {
- "type": "File",
- "label": "Single File",
- "doc": "This should allow for single File selection only.",
- "id": "#main/single_file"
- }
- ],
- "outputs": [
- {
- "type": "File",
- "outputSource": "#main/input_collector/output",
- "id": "#main/log_file"
- }
- ],
- "steps": [
- {
- "run": "#input_collector.cwl",
- "in": [
- {
- "source": "#main/single_file",
- "id": "#main/input_collector/single_file"
- }
- ],
- "out": [
- "#main/input_collector/output"
- ],
- "id": "#main/input_collector"
- }
- ],
- "id": "#main"
- }
- ]
- },
- },
- '/var/lib/cwl/cwl.input.json': {
- kind: MountKind.JSON,
- content: {
- "single_file": {
- "class": "File",
- "location": "keep:233454526794c0a2d56a305baeff3d30+145/1.txt",
- "basename": "fileA"
- }
- },
- }
- },
- runtimeConstraints: {
- API: true,
- vcpus: 1,
- ram: 1073741824,
- },
- containerImage: 'arvados/jobs:1.1.4.20180618144723',
- cwd: '/var/spool/cwl',
- command: [
- 'arvados-cwl-runner',
- '--local',
- '--api=containers',
- "--project-uuid=c97qk-j7d0g-s3ngc1z0748hsmf",
- '/var/lib/cwl/workflow.json#main',
- '/var/lib/cwl/cwl.input.json'
- ],
- outputPath: '/var/spool/cwl',
- priority: 1,
- });
-};
-
// force build comment #1
import {
StepModel,
WorkflowInputParameterModel,
- WorkflowOutputParameterModel,
- WorkflowStepInputModel,
- WorkflowStepOutputModel
+ WorkflowOutputParameterModel
} from "cwlts/models";
export class SVGArrangePlugin implements SVGPlugin {
version: number;
preserveVersion: boolean;
unsignedManifestText?: string;
+ fileCount: number;
+ fileSizeTotal: number;
}
export const getCollectionUrl = (uuid: string) => {
groupClass: GroupClass | null;
description: string;
properties: any;
- writeableBy: string[];
+ writableBy: string[];
ensure_unique_name: boolean;
}
export interface CollectionMount {
kind: MountKind.COLLECTION;
uuid?: string;
- portableDataHash?: string;
+ portable_data_hash?: string;
path?: string;
writable?: boolean;
}
}
export interface NodeInfo {
- lastAction: string;
- pingSecret: string;
- ec2InstanceId: string;
- slurmState?: string;
+ last_action: string;
+ ping_secret: string;
+ ec2_instance_id: string;
+ slurm_state?: string;
}
export interface NodeProperties {
- cloudNode: CloudNode;
- totalRamMb: number;
- totalCpuCores: number;
- totalScratchMb: number;
+ cloud_node: CloudNode;
+ total_ram_mb: number;
+ total_cpu_cores: number;
+ total_scratch_mb: number;
}
interface CloudNode {
export type ProcessResource = ContainerRequestResource;
+export const MOUNT_PATH_CWL_WORKFLOW = '/var/lib/cwl/workflow.json';
+export const MOUNT_PATH_CWL_INPUT = '/var/lib/cwl/cwl.input.json';
+
export const createWorkflowMounts = (workflow: WorkflowResource, inputs: WorkflowInputsData): { [path: string]: MountType } => {
return {
'/var/spool/cwl': {
}
export const RESOURCE_UUID_PATTERN = '[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}';
-export const RESOURCE_UUID_REGEX = new RegExp(RESOURCE_UUID_PATTERN);
-export const COLLECTION_PDH_REGEX = /[a-f0-9]{32}\+\d+/;
+export const RESOURCE_UUID_REGEX = new RegExp("^" + RESOURCE_UUID_PATTERN + "$");
+export const COLLECTION_PDH_REGEX = /^[a-f0-9]{32}\+\d+$/;
export const isResourceUuid = (uuid: string) =>
RESOURCE_UUID_REGEX.test(uuid);
export interface RuntimeConstraints {
ram: number;
vcpus: number;
- keepCacheRam?: number;
+ keep_cache_ram?: number;
API: boolean;
}
export interface SchedulingParameters {
partitions?: string[];
preemptible?: boolean;
- maxRunTime?: number;
+ max_run_time?: number;
}
import { ResourceKind } from '~/models/resource';
-export type SearchBarAdvanceFormData = {
+export type SearchBarAdvancedFormData = {
type?: ResourceKind;
cluster?: string;
projectUuid?: string;
export interface PropertyValue {
key: string;
+ keyID?: string;
value: string;
+ valueID?: string;
}
clusterId: string;
remoteHost: string;
baseUrl: string;
- username: string;
+ name: string;
email: string;
token: string;
+ uuid: string;
loggedIn: boolean;
status: SessionStatus;
active: boolean;
export interface TagProperty {
key: string;
+ keyID?: string;
value: string;
+ valueID?: string;
}
export enum TagTailType {
properties: "",
trashAt: "",
uuid: "",
- writeableBy: [],
+ writableBy: [],
ensure_unique_name: true,
...data
});
const addChild = (parentId: string, childId: string) => <T>(tree: Tree<T>): Tree<T> => {
+ if (childId === "") {
+ return tree;
+ }
const node = getNode(parentId)(tree);
if (node) {
const children = node.children.some(id => id === childId)
return user ? `${user.firstName} ${user.lastName}` : "";
};
-export interface UserResource extends Resource {
+export interface UserResource extends Resource, User {
kind: ResourceKind.USER;
- email: string;
- username: string;
- firstName: string;
- lastName: string;
- isAdmin: boolean;
- prefs: UserPrefs;
defaultOwnerUuid: string;
- isActive: boolean;
writableBy: string[];
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as Vocabulary from './vocabulary';
+import { pipe } from 'lodash/fp';
+
+describe('Vocabulary', () => {
+ let vocabulary: Vocabulary.Vocabulary;
+
+ beforeEach(() => {
+ vocabulary = {
+ strict_tags: false,
+ tags: {
+ IDKEYCOMMENT: {
+ labels: []
+ },
+ IDKEYANIMALS: {
+ strict: false,
+ labels: [
+ {label: "Animal" },
+ {label: "Creature"}
+ ],
+ values: {
+ IDVALANIMALS1: {
+ labels: [
+ {label: "Human"},
+ {label: "Homo sapiens"}
+ ]
+ },
+ IDVALANIMALS2: {
+ labels: [
+ {label: "Dog"},
+ {label: "Canis lupus familiaris"}
+ ]
+ },
+ }
+ },
+ IDKEYSIZES: {
+ labels: [{label: "Sizes"}],
+ values: {
+ IDVALSIZES1: {
+ labels: [{label: "Small"}]
+ },
+ IDVALSIZES2: {
+ labels: [{label: "Medium"}]
+ },
+ IDVALSIZES3: {
+ labels: [{label: "Large"}]
+ },
+ IDVALSIZES4: {
+ labels: []
+ }
+ }
+ }
+ }
+ }
+ });
+
+ it('returns the list of tag keys', () => {
+ const tagKeys = Vocabulary.getTags(vocabulary);
+ // Alphabetically ordered by label
+ expect(tagKeys).toEqual([
+ {id: "IDKEYANIMALS", label: "Animal"},
+ {id: "IDKEYANIMALS", label: "Creature"},
+ {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT"},
+ {id: "IDKEYSIZES", label: "Sizes"},
+ ]);
+ });
+
+ it('returns the tag values for a given key', () => {
+ const tagValues = Vocabulary.getTagValues('IDKEYSIZES', vocabulary);
+ // Alphabetically ordered by label
+ expect(tagValues).toEqual([
+ {id: "IDVALSIZES4", label: "IDVALSIZES4"},
+ {id: "IDVALSIZES3", label: "Large"},
+ {id: "IDVALSIZES2", label: "Medium"},
+ {id: "IDVALSIZES1", label: "Small"},
+ ])
+ });
+
+ it('returns an empty list of values for an non-existent key', () => {
+ const tagValues = Vocabulary.getTagValues('IDNONSENSE', vocabulary);
+ expect(tagValues).toEqual([]);
+ });
+
+ it('returns a key id for a given key label', () => {
+ const testCases = [
+ // Two labels belonging to the same ID
+ {keyLabel: 'Animal', expected: 'IDKEYANIMALS'},
+ {keyLabel: 'Creature', expected: 'IDKEYANIMALS'},
+ // Non-existent label returns empty string
+ {keyLabel: 'ThisKeyLabelDoesntExist', expected: ''},
+ ]
+ testCases.forEach(tc => {
+ const tagValueID = Vocabulary.getTagKeyID(tc.keyLabel, vocabulary);
+ expect(tagValueID).toEqual(tc.expected);
+ });
+ });
+
+ it('returns an key label for a given key id', () => {
+ const testCases = [
+ // ID with many labels return the first one
+ {keyID: 'IDKEYANIMALS', expected: 'Animal'},
+ // Key IDs without any labels or unknown keys should return the literal
+ // key from the API's response (that is, the key 'id')
+ {keyID: 'IDKEYCOMMENT', expected: 'IDKEYCOMMENT'},
+ {keyID: 'FOO', expected: 'FOO'},
+ ]
+ testCases.forEach(tc => {
+ const tagValueID = Vocabulary.getTagKeyLabel(tc.keyID, vocabulary);
+ expect(tagValueID).toEqual(tc.expected);
+ });
+ });
+
+ it('returns a value id for a given key id and value label', () => {
+ const testCases = [
+ // Key ID and value label known
+ {keyID: 'IDKEYANIMALS', valueLabel: 'Human', expected: 'IDVALANIMALS1'},
+ {keyID: 'IDKEYANIMALS', valueLabel: 'Homo sapiens', expected: 'IDVALANIMALS1'},
+ // Key ID known, value label unknown
+ {keyID: 'IDKEYANIMALS', valueLabel: 'Dinosaur', expected: ''},
+ // Key ID unknown
+ {keyID: 'IDNONSENSE', valueLabel: 'Does not matter', expected: ''},
+ ]
+ testCases.forEach(tc => {
+ const tagValueID = Vocabulary.getTagValueID(tc.keyID, tc.valueLabel, vocabulary);
+ expect(tagValueID).toEqual(tc.expected);
+ });
+ });
+
+ it('returns a value label for a given key & value id pair', () => {
+ const testCases = [
+ // Known key & value ids with multiple value labels: returns the first label
+ {keyId: 'IDKEYANIMALS', valueId: 'IDVALANIMALS1', expected: 'Human'},
+ // Values without label or unknown values should return the literal value from
+ // the API's response (that is, the value 'id')
+ {keyId: 'IDKEYSIZES', valueId: 'IDVALSIZES4', expected: 'IDVALSIZES4'},
+ {keyId: 'IDKEYCOMMENT', valueId: 'FOO', expected: 'FOO'},
+ {keyId: 'IDKEYANIMALS', valueId: 'BAR', expected: 'BAR'},
+ {keyId: 'IDKEYNONSENSE', valueId: 'FOOBAR', expected: 'FOOBAR'},
+ ]
+ testCases.forEach(tc => {
+ const tagValueLabel = Vocabulary.getTagValueLabel(tc.keyId, tc.valueId, vocabulary);
+ expect(tagValueLabel).toEqual(tc.expected);
+ });
+ });
+});
import { isObject, has, every } from 'lodash/fp';
export interface Vocabulary {
- strict: boolean;
+ strict_tags: boolean;
tags: Record<string, Tag>;
}
+export interface Label {
+ lang?: string;
+ label: string;
+}
+
+export interface TagValue {
+ labels: Label[];
+}
+
export interface Tag {
strict?: boolean;
- values?: string[];
+ labels: Label[];
+ values?: Record<string, TagValue>;
+}
+
+export interface PropFieldSuggestion {
+ id: string;
+ label: string;
}
const VOCABULARY_VALIDATORS = [
isObject,
- has('strict'),
+ has('strict_tags'),
has('tags'),
];
export const isVocabulary = (value: any) =>
- every(validator => validator(value), VOCABULARY_VALIDATORS);
\ No newline at end of file
+ every(validator => validator(value), VOCABULARY_VALIDATORS);
+
+export const isStrictTag = (tagKeyID: string, vocabulary: Vocabulary) => {
+ const tag = vocabulary.tags[tagKeyID];
+ return tag ? tag.strict : false;
+};
+
+export const getTagValueID = (tagKeyID:string, tagValueLabel:string, vocabulary: Vocabulary) =>
+ (tagKeyID && vocabulary.tags[tagKeyID] && vocabulary.tags[tagKeyID].values)
+ ? Object.keys(vocabulary.tags[tagKeyID].values!).find(
+ k => vocabulary.tags[tagKeyID].values![k].labels.find(
+ l => l.label === tagValueLabel) !== undefined) || ''
+ : '';
+
+export const getTagValueLabel = (tagKeyID:string, tagValueID:string, vocabulary: Vocabulary) =>
+ vocabulary.tags[tagKeyID] &&
+ vocabulary.tags[tagKeyID].values &&
+ vocabulary.tags[tagKeyID].values![tagValueID] &&
+ vocabulary.tags[tagKeyID].values![tagValueID].labels.length > 0
+ ? vocabulary.tags[tagKeyID].values![tagValueID].labels[0].label
+ : tagValueID;
+
+const compare = (a: PropFieldSuggestion, b: PropFieldSuggestion) => {
+ if (a.label < b.label) {return -1;}
+ if (a.label > b.label) {return 1;}
+ return 0;
+};
+
+export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary) => {
+ const tag = vocabulary.tags[tagKeyID];
+ const ret = tag && tag.values
+ ? Object.keys(tag.values).map(
+ tagValueID => tag.values![tagValueID].labels && tag.values![tagValueID].labels.length > 0
+ ? tag.values![tagValueID].labels.map(
+ lbl => Object.assign({}, {"id": tagValueID, "label": lbl.label}))
+ : [{"id": tagValueID, "label": tagValueID}])
+ .reduce((prev, curr) => [...prev, ...curr], [])
+ .sort(compare)
+ : [];
+ return ret;
+};
+
+export const getTags = ({ tags }: Vocabulary) => {
+ const ret = tags && Object.keys(tags)
+ ? Object.keys(tags).map(
+ tagID => tags[tagID].labels && tags[tagID].labels.length > 0
+ ? tags[tagID].labels.map(
+ lbl => Object.assign({}, {"id": tagID, "label": lbl.label}))
+ : [{"id": tagID, "label": tagID}])
+ .reduce((prev, curr) => [...prev, ...curr], [])
+ .sort(compare)
+ : [];
+ return ret;
+};
+
+export const getTagKeyID = (tagKeyLabel:string, vocabulary: Vocabulary) =>
+ Object.keys(vocabulary.tags).find(
+ k => vocabulary.tags[k].labels.find(
+ l => l.label === tagKeyLabel) !== undefined
+ ) || '';
+
+export const getTagKeyLabel = (tagKeyID:string, vocabulary: Vocabulary) =>
+ vocabulary.tags[tagKeyID] && vocabulary.tags[tagKeyID].labels.length > 0
+ ? vocabulary.tags[tagKeyID].labels[0].label
+ : tagKeyID;
description: string;
definition: string;
}
-export interface WorkflowResoruceDefinition {
+export interface WorkflowResourceDefinition {
cwlVersion: string;
- graph?: Array<Workflow | CommandLineTool>;
$graph?: Array<Workflow | CommandLineTool>;
}
export interface Workflow {
export type WorkflowInputsData = {
[key: string]: boolean | number | string | File | Directory;
};
-export const parseWorkflowDefinition = (workflow: WorkflowResource): WorkflowResoruceDefinition => {
+export const parseWorkflowDefinition = (workflow: WorkflowResource): WorkflowResourceDefinition => {
const definition = safeLoad(workflow.definition);
return definition;
};
-export const getWorkflowInputs = (workflowDefinition: WorkflowResoruceDefinition) => {
- if (workflowDefinition.graph) {
- const mainWorkflow = workflowDefinition.graph.find(item => item.class === 'Workflow' && item.id === '#main');
- return mainWorkflow
- ? mainWorkflow.inputs
- : undefined;
- } else {
- const mainWorkflow = workflowDefinition.$graph!.find(item => item.class === 'Workflow' && item.id === '#main');
- return mainWorkflow
- ? mainWorkflow.inputs
- : undefined;
- }
+export const getWorkflow = (workflowDefinition: WorkflowResourceDefinition) => {
+ if (!workflowDefinition.$graph) { return undefined; }
+ const mainWorkflow = workflowDefinition.$graph.find(item => item.class === 'Workflow' && item.id === '#main');
+ return mainWorkflow
+ ? mainWorkflow
+ : undefined;
+};
+
+export const getWorkflowInputs = (workflowDefinition: WorkflowResourceDefinition) => {
+ if (!workflowDefinition) { return undefined; }
+ return getWorkflow(workflowDefinition)
+ ? getWorkflow(workflowDefinition)!.inputs
+ : undefined;
};
export const getInputLabel = (input: CommandInputParameter) => {
ROOT: '/',
TOKEN: '/token',
FED_LOGIN: '/fedtoken',
+ ADD_SESSION: '/add-session',
PROJECTS: `/projects/:id(${RESOURCE_UUID_PATTERN})`,
COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
} else if (config.remoteHostsConfig[cls]) {
let u: URL;
if (config.remoteHostsConfig[cls].workbench2Url) {
- u = new URL(config.remoteHostsConfig[cls].workbench2Url || "");
+ /* NOTE: wb2 presently doesn't support passing api_token
+ to arbitrary page to set credentials, only through
+ api-token route. So for navigation to work, user needs
+ to already be logged in. In the future we want to just
+ request the records and display in the current
+ workbench instance making this redirect unnecessary. */
+ 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;
matchPath(route, { path: Routes.TOKEN });
export const matchFedTokenRoute = (route: string) =>
- matchPath(route, {path: Routes.FED_LOGIN});
+ matchPath(route, { path: Routes.FED_LOGIN });
export const matchUsersRoute = (route: string) =>
matchPath(route, { path: Routes.USERS });
new FilterBuilder()
.addFullTextSearch('my custom search')
.getFilters()
- ).toEqual(`["any","@@","my:*&custom:*&search"]`);
+ ).toEqual(`["any","ilike","%my%"],["any","ilike","%custom%"],["any","ilike","%search%"]`);
});
});
}
public addFullTextSearch(value: string) {
- // Filter construction implementation taken from
- // https://dev.arvados.org/projects/arvados/repository/entry/apps/workbench/app/assets/javascripts/filterable.js
- // https://dev.arvados.org/projects/arvados/repository/entry/apps/workbench/app/assets/javascripts/to_tsquery.js
- return this.addCondition('any', '@@', value.replace(/[^-\w\.\/]+/g, ' ').trim().replace(/ /g, ':*&'));
+ const terms = value.trim().split(/(\s+)/);
+ terms.forEach(term => {
+ if (term !== " ") {
+ this.addCondition("any", "ilike", term, "%", "%");
+ }
+ });
+ return this;
}
public getFilters() {
//
// SPDX-License-Identifier: AGPL-3.0
-import { OrderBuilder } from "./order-builder";
import { joinUrls } from "~/services/api/url-builder";
describe("UrlBuilder", () => {
//
// SPDX-License-Identifier: AGPL-3.0
-import { getUserFullname, User, UserPrefs, UserResource } from '~/models/user';
+import { getUserFullname, User, UserPrefs } from '~/models/user';
import { AxiosInstance } from "axios";
import { ApiActions } from "~/services/api/api-actions";
import * as uuid from "uuid/v4";
public saveApiToken(token: string) {
localStorage.setItem(API_TOKEN_KEY, token);
- localStorage.setItem(HOME_CLUSTER, token.split('/')[1].substr(0, 5));
+ const sp = token.split('/');
+ if (sp.length === 3) {
+ localStorage.setItem(HOME_CLUSTER, sp[1].substr(0, 5));
+ }
}
public removeApiToken() {
return localStorage.getItem(HOME_CLUSTER) || undefined;
}
- public getUuid() {
- return localStorage.getItem(USER_UUID_KEY) || undefined;
- }
-
- public getOwnerUuid() {
- return localStorage.getItem(USER_OWNER_UUID_KEY) || undefined;
- }
-
- public getIsAdmin(): boolean {
- return localStorage.getItem(USER_IS_ADMIN) === 'true';
- }
-
- public getIsActive(): boolean {
- return localStorage.getItem(USER_IS_ACTIVE) === 'true';
- }
-
- public getUser(): User | undefined {
- const email = localStorage.getItem(USER_EMAIL_KEY);
- const firstName = localStorage.getItem(USER_FIRST_NAME_KEY);
- const lastName = localStorage.getItem(USER_LAST_NAME_KEY);
- const uuid = this.getUuid();
- const ownerUuid = this.getOwnerUuid();
- const isAdmin = this.getIsAdmin();
- const isActive = this.getIsActive();
- const username = localStorage.getItem(USER_USERNAME);
- const prefs = JSON.parse(localStorage.getItem(USER_PREFS) || '{"profile": {}}');
-
- return email && firstName && lastName && uuid && ownerUuid && username && prefs
- ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, isActive, username, prefs }
- : undefined;
- }
-
- public saveUser(user: User | UserResource) {
- localStorage.setItem(USER_EMAIL_KEY, user.email);
- localStorage.setItem(USER_FIRST_NAME_KEY, user.firstName);
- localStorage.setItem(USER_LAST_NAME_KEY, user.lastName);
- localStorage.setItem(USER_UUID_KEY, user.uuid);
- localStorage.setItem(USER_OWNER_UUID_KEY, user.ownerUuid);
- localStorage.setItem(USER_IS_ADMIN, JSON.stringify(user.isAdmin));
- localStorage.setItem(USER_IS_ACTIVE, JSON.stringify(user.isActive));
- localStorage.setItem(USER_USERNAME, user.username);
- localStorage.setItem(USER_PREFS, JSON.stringify(user.prefs));
- }
-
public removeUser() {
localStorage.removeItem(USER_EMAIL_KEY);
localStorage.removeItem(USER_FIRST_NAME_KEY);
localStorage.removeItem(USER_PREFS);
}
- public login(uuidPrefix: string, homeCluster: string, remoteHosts: { [key: string]: string }) {
+ public login(uuidPrefix: string, homeCluster: string, loginCluster: string, remoteHosts: { [key: string]: string }) {
const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
const homeClusterHost = remoteHosts[homeCluster];
- window.location.assign(`https://${homeClusterHost}/login?${uuidPrefix !== homeCluster ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
+ window.location.assign(`https://${homeClusterHost}/login?${(uuidPrefix !== homeCluster && homeCluster !== loginCluster) ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
}
public logout() {
});
}
- public getRootUuid() {
- const uuid = this.getOwnerUuid();
- const uuidParts = uuid ? uuid.split('-') : [];
- return uuidParts.length > 1 ? `${uuidParts[0]}-${uuidParts[1]}` : undefined;
- }
-
public getSessions(): Session[] {
try {
const sessions = JSON.parse(localStorage.getItem("sessions") || '');
clusterId: cfg.uuidPrefix,
remoteHost: cfg.rootUrl,
baseUrl: cfg.baseUrl,
- username: getUserFullname(user),
+ name: getUserFullname(user),
email: user ? user.email : '',
token: this.getApiToken(),
loggedIn: true,
active: true,
+ uuid: user ? user.uuid : '',
status: SessionStatus.VALIDATED
} as Session;
- const localSessions = this.getSessions();
+ const localSessions = this.getSessions().map(s => ({
+ ...s,
+ active: false,
+ status: SessionStatus.INVALIDATED
+ }));
+
const cfgSessions = Object.keys(cfg.remoteHosts).map(clusterId => {
const remoteHost = cfg.remoteHosts[clusterId];
return {
clusterId,
remoteHost,
baseUrl: '',
- username: '',
+ name: '',
email: '',
token: '',
loggedIn: false,
active: false,
+ uuid: '',
status: SessionStatus.INVALIDATED
} as Session;
});
const sessions = [currentSession]
+ .concat(cfgSessions)
.concat(localSessions)
- .concat(cfgSessions);
+ .filter((r: Session) => r.clusterId !== "*");
const uniqSessions = uniqBy(sessions, 'clusterId');
import { CollectionService } from "../collection-service/collection-service";
import { parseKeepManifestText, stringifyKeepManifest } from "./collection-manifest-parser";
import { mapManifestToCollectionFilesTree } from "./collection-manifest-mapper";
-import { CommonResourceService } from "~/services/common-service/common-resource-service";
-import * as _ from "lodash";
export class CollectionFilesService {
: stream
);
const manifestText = stringifyKeepManifest(updatedManifest);
- const data = { ...collection, manifestText };
- return this.collectionService.update(collectionUuid, CommonResourceService.mapKeys(_.snakeCase)(data));
+ return this.collectionService.update(collectionUuid, { manifestText });
}
async deleteFile(collectionUuid: string, file: { name: string, path: string }) {
const manifestText = stringifyKeepManifest(updatedManifest);
return this.collectionService.update(collectionUuid, { manifestText });
}
-
- renameTest() {
- const u = this.renameFile('qr1hi-4zz18-n0sx074erl4p0ph', {
- name: 'extracted2.txt.png',
- path: ''
- }, 'extracted-new.txt.png');
- }
}
//
// SPDX-License-Identifier: AGPL-3.0
-import { createCollectionFilesTree, CollectionDirectory, CollectionFile, CollectionFileType, createCollectionDirectory, createCollectionFile } from "../../models/collection-file";
+import { CollectionDirectory, CollectionFile, CollectionFileType, createCollectionDirectory, createCollectionFile } from "../../models/collection-file";
import { getTagValue } from "~/common/xml";
import { getNodeChildren, Tree, mapTree } from '~/models/tree';
-export const parseFilesResponse = (document: Document) => {
- const files = extractFilesData(document);
- const tree = createCollectionFilesTree(files);
- return sortFilesTree(tree);
-};
-
export const sortFilesTree = (tree: Tree<CollectionDirectory | CollectionFile>) => {
return mapTree<CollectionDirectory | CollectionFile>(node => {
const children = getNodeChildren(node.id)(tree);
.replace(collectionUrlPrefix, '')
.replace(nameSuffix, '');
-
+ const parentPath = directory.replace(/\/$/, '');
const data = {
url,
id: [
collectionUuid ? collectionUuid : '',
- directory ? '/' + directory.replace(/^\//, '') : '',
+ directory ? parentPath : '',
'/' + name
].join(''),
name,
- path: directory,
+ path: parentPath,
};
return getTagValue(element, 'D:resourcetype', '')
import { CollectionFile, CollectionDirectory } from "~/models/collection-file";
import { WebDAV } from "~/common/webdav";
import { AuthService } from "../auth-service/auth-service";
-import { mapTreeValues } from "~/models/tree";
-import { parseFilesResponse } from "./collection-service-files-response";
-import { fileToArrayBuffer } from "~/common/file";
+import { extractFilesData } from "./collection-service-files-response";
import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
import { ApiActions } from "~/services/api/api-actions";
-import { snakeCase } from 'lodash';
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
async files(uuid: string) {
const request = await this.webdavClient.propfind(`c=${uuid}`);
if (request.responseXML != null) {
- const filesTree = parseFilesResponse(request.responseXML);
- return mapTreeValues(this.extendFileURL)(filesTree);
+ return extractFilesData(request.responseXML);
}
return Promise.reject();
}
);
}
- private extendFileURL = (file: CollectionDirectory | CollectionFile) => {
+ extendFileURL = (file: CollectionDirectory | CollectionFile) => {
const baseUrl = this.webdavClient.defaults.baseURL.endsWith('/')
? this.webdavClient.defaults.baseURL.slice(0, -1)
: this.webdavClient.defaults.baseURL;
};
return this.webdavClient.upload(fileURL, [file], requestConfig);
}
-
- update(uuid: string, data: Partial<CollectionResource>) {
- if (uuid && data && data.properties) {
- const { properties } = data;
- const mappedData = {
- ...TrashableResourceService.mapKeys(snakeCase)(data),
- properties,
- };
- return TrashableResourceService
- .defaultResponse(
- this.serverApi
- .put<CollectionResource>(this.resourceType + uuid, mappedData),
- this.actions,
- false
- );
- }
- return TrashableResourceService
- .defaultResponse(
- this.serverApi
- .put<CollectionResource>(this.resourceType + uuid, data && TrashableResourceService.mapKeys(snakeCase)(data)),
- this.actions
- );
- }
}
it("#create", async () => {
axiosMock
- .onPost("/resource/")
+ .onPost("/resource")
.reply(200, { owner_uuid: "ownerUuidValue" });
const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
axiosInstance.post = jest.fn(() => Promise.resolve({data: {}}));
const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
- expect(axiosInstance.post).toHaveBeenCalledWith("/resource/", {owner_uuid: "ownerUuidValue"});
+ expect(axiosInstance.post).toHaveBeenCalledWith("/resource", {owner_uuid: "ownerUuidValue"});
});
it("#delete", async () => {
it("#get", async () => {
axiosMock
.onGet("/resource/uuid")
- .reply(200, { modified_at: "now" });
+ .reply(200, {
+ modified_at: "now",
+ properties: {
+ responsible_owner_uuid: "another_owner"
+ }
+ });
const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
const resource = await commonResourceService.get("uuid");
- expect(resource).toEqual({ modifiedAt: "now" });
+ // Only first level keys are mapped to camel case
+ expect(resource).toEqual({
+ modifiedAt: "now",
+ properties: {
+ responsible_owner_uuid: "another_owner"
+ }
+ });
});
it("#list", async () => {
axiosMock
- .onGet("/resource/")
+ .onGet("/resource")
.reply(200, {
kind: "kind",
offset: 2,
limit: 10,
items: [{
- modified_at: "now"
+ modified_at: "now",
+ properties: {
+ is_active: true
+ }
}],
items_available: 20
});
const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
const resource = await commonResourceService.list({ limit: 10, offset: 1 });
+ // First level keys are mapped to camel case inside "items" arrays
expect(resource).toEqual({
kind: "kind",
offset: 2,
limit: 10,
items: [{
- modifiedAt: "now"
+ modifiedAt: "now",
+ properties: {
+ is_active: true
+ }
}],
itemsAvailable: 20
});
import { CommonService } from "~/services/common-service/common-service";
export enum CommonResourceServiceError {
- UNIQUE_VIOLATION = 'UniqueViolation',
+ UNIQUE_NAME_VIOLATION = 'UniqueNameViolation',
OWNERSHIP_CYCLE = 'OwnershipCycle',
MODIFYING_CONTAINER_REQUEST_FINAL_STATE = 'ModifyingContainerRequestFinalState',
NAME_HAS_ALREADY_BEEN_TAKEN = 'NameHasAlreadyBeenTaken',
const error = errorResponse.errors.join('');
switch (true) {
case /UniqueViolation/.test(error):
- return CommonResourceServiceError.UNIQUE_VIOLATION;
+ return CommonResourceServiceError.UNIQUE_NAME_VIOLATION;
case /ownership cycle/.test(error):
return CommonResourceServiceError.OWNERSHIP_CYCLE;
case /Mounts cannot be modified in state 'Final'/.test(error):
constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
this.serverApi = serverApi;
- this.resourceType = '/' + resourceType + '/';
+ this.resourceType = '/' + resourceType;
this.actions = actions;
}
.map(key => [key, mapFn(key)])
.reduce((newValue, [key, newKey]) => ({
...newValue,
- [newKey]: CommonService.mapKeys(mapFn)(value[key])
+ [newKey]: (key === 'items') ? CommonService.mapKeys(mapFn)(value[key]) : value[key]
}), {});
case _.isArray(value):
return value.map(CommonService.mapKeys(mapFn));
delete(uuid: string): Promise<T> {
return CommonService.defaultResponse(
this.serverApi
- .delete(this.resourceType + uuid),
+ .delete(this.resourceType + '/' + uuid),
this.actions
);
}
get(uuid: string) {
return CommonService.defaultResponse(
this.serverApi
- .get<T>(this.resourceType + uuid),
+ .get<T>(this.resourceType + '/' + uuid),
this.actions
);
}
update(uuid: string, data: Partial<T>) {
return CommonService.defaultResponse(
this.serverApi
- .put<T>(this.resourceType + uuid, data && CommonService.mapKeys(_.snakeCase)(data)),
+ .put<T>(this.resourceType + '/' + uuid, data && CommonService.mapKeys(_.snakeCase)(data)),
this.actions
);
}
trash(uuid: string): Promise<T> {
return CommonResourceService.defaultResponse(
this.serverApi
- .post(this.resourceType + `${uuid}/trash`),
+ .post(this.resourceType + `/${uuid}/trash`),
this.actions
);
}
};
return CommonResourceService.defaultResponse(
this.serverApi
- .post(this.resourceType + `${uuid}/untrash`, {
+ .post(this.resourceType + `/${uuid}/untrash`, {
params: CommonResourceService.mapKeys(_.snakeCase)(params)
}),
this.actions
//
// SPDX-License-Identifier: AGPL-3.0
-import { snakeCase } from 'lodash';
import { CommonResourceService } from "~/services/common-service/common-resource-service";
import { AxiosInstance } from "axios";
import { ContainerRequestResource } from '~/models/container-request';
constructor(serverApi: AxiosInstance, actions: ApiActions) {
super(serverApi, "container_requests", actions);
}
-
- create(data?: Partial<ContainerRequestResource>) {
- if (data) {
- const { mounts } = data;
- if (mounts) {
- const mappedData = {
- ...CommonResourceService.mapKeys(snakeCase)(data),
- mounts,
- };
- return CommonResourceService
- .defaultResponse(
- this.serverApi.post<ContainerRequestResource>(this.resourceType, mappedData),
- this.actions);
- }
- }
- return CommonResourceService
- .defaultResponse(
- this.serverApi
- .post<ContainerRequestResource>(this.resourceType, data && CommonResourceService.mapKeys(snakeCase)(data)),
- this.actions);
- }
}
import { CollectionResource } from "~/models/collection";
import { ProjectResource } from "~/models/project";
import { ProcessResource } from "~/models/process";
-import { ResourceKind } from '~/models/resource';
import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
import { ApiActions } from "~/services/api/api-actions";
import { GroupResource } from "~/models/group";
filters: filters ? `[${filters}]` : undefined,
order: order ? order : undefined
};
-
- const pathUrl = uuid ? `${uuid}/contents` : 'contents';
+ const pathUrl = uuid ? `/${uuid}/contents` : '/contents';
const cfg: AxiosRequestConfig = { params: CommonResourceService.mapKeys(_.snakeCase)(params) };
if (session) {
cfg.baseURL = session.baseUrl;
+ cfg.headers = { 'Authorization': 'Bearer ' + session.token };
}
const response = await CommonResourceService.defaultResponse(
this.serverApi.get(this.resourceType + pathUrl, cfg), this.actions, false
);
- const { items, ...res } = response;
- const mappedItems = (items || []).map((item: GroupContentsResource) => {
- const mappedItem = TrashableResourceService.mapKeys(_.camelCase)(item);
- if (item.kind === ResourceKind.COLLECTION || item.kind === ResourceKind.PROJECT) {
- const { properties } = item;
- return { ...mappedItem, properties };
- } else {
- return mappedItem;
- }
- });
- const mappedResponse = { ...TrashableResourceService.mapKeys(_.camelCase)(res) };
- return { ...mappedResponse, items: mappedItems, clusterId: session && session.clusterId };
+ return { ...TrashableResourceService.mapKeys(_.camelCase)(response), clusterId: session && session.clusterId };
}
shared(params: SharedArguments = {}): Promise<ListResults<GroupContentsResource>> {
return CommonResourceService.defaultResponse(
this.serverApi
- .get(this.resourceType + 'shared', { params }),
+ .get(this.resourceType + '/shared', { params }),
this.actions
);
}
redirect_to_new_user: true
};
return CommonService.defaultResponse(
- this.serverApi.post('/users/merge/', params),
+ this.serverApi.post('/users/merge', params),
this.actions,
false
);
}
-}
\ No newline at end of file
+}
axiosInstance.post = jest.fn(() => Promise.resolve({ data: {} }));
const projectService = new ProjectService(axiosInstance, actions);
const resource = await projectService.create({ name: "nameValue" });
- expect(axiosInstance.post).toHaveBeenCalledWith("/groups/", {
+ expect(axiosInstance.post).toHaveBeenCalledWith("/groups", {
name: "nameValue",
group_class: "project"
});
axiosInstance.get = jest.fn(() => Promise.resolve({ data: {} }));
const projectService = new ProjectService(axiosInstance, actions);
const resource = await projectService.list();
- expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
+ expect(axiosInstance.get).toHaveBeenCalledWith("/groups", {
params: {
filters: "[" + new FilterBuilder()
.addEqual("groupClass", "project")
import { GroupClass } from "~/models/group";
import { ListArguments } from "~/services/common-service/common-service";
import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
-import { TrashableResourceService } from '~/services/common-service/trashable-resource-service';
-import { snakeCase } from 'lodash';
export class ProjectService extends GroupsService<ProjectResource> {
create(data: Partial<ProjectResource>) {
return super.create(projectData);
}
- update(uuid: string, data: Partial<ProjectResource>) {
- if (uuid && data && data.properties) {
- const { properties } = data;
- const mappedData = {
- ...TrashableResourceService.mapKeys(snakeCase)(data),
- properties,
- };
- return TrashableResourceService
- .defaultResponse(
- this.serverApi
- .put<ProjectResource>(this.resourceType + uuid, mappedData),
- this.actions,
- false
- );
- }
- return TrashableResourceService
- .defaultResponse(
- this.serverApi
- .put<ProjectResource>(this.resourceType + uuid, data && TrashableResourceService.mapKeys(snakeCase)(data)),
- this.actions
- );
- }
-
list(args: ListArguments = {}) {
return super.list({
...args,
//
// SPDX-License-Identifier: AGPL-3.0
-import { SearchBarAdvanceFormData } from '~/models/search-bar';
+import { SearchBarAdvancedFormData } from '~/models/search-bar';
export class SearchService {
private recentQueries = this.getRecentQueries();
- private savedQueries: SearchBarAdvanceFormData[] = this.getSavedQueries();
+ private savedQueries: SearchBarAdvancedFormData[] = this.getSavedQueries();
saveRecentQuery(query: string) {
if (this.recentQueries.length >= MAX_NUMBER_OF_RECENT_QUERIES) {
return JSON.parse(localStorage.getItem('recentQueries') || '[]');
}
- saveQuery(data: SearchBarAdvanceFormData) {
+ saveQuery(data: SearchBarAdvancedFormData) {
this.savedQueries.push({...data});
localStorage.setItem('savedQueries', JSON.stringify(this.savedQueries));
}
- editSavedQueries(data: SearchBarAdvanceFormData) {
+ editSavedQueries(data: SearchBarAdvancedFormData) {
const itemIndex = this.savedQueries.findIndex(item => item.queryName === data.queryName);
this.savedQueries[itemIndex] = {...data};
localStorage.setItem('savedQueries', JSON.stringify(this.savedQueries));
}
getSavedQueries() {
- return JSON.parse(localStorage.getItem('savedQueries') || '[]') as SearchBarAdvanceFormData[];
+ return JSON.parse(localStorage.getItem('savedQueries') || '[]') as SearchBarAdvancedFormData[];
}
deleteSavedQuery(id: number) {
// SPDX-License-Identifier: AGPL-3.0
import Axios from "axios";
+import { AxiosInstance } from "axios";
import { ApiClientAuthorizationService } from '~/services/api-client-authorization-service/api-client-authorization-service';
import { AuthService } from "./auth-service/auth-service";
import { GroupsService } from "./groups-service/groups-service";
export type ServiceRepository = ReturnType<typeof createServices>;
-export const createServices = (config: Config, actions: ApiActions) => {
- const apiClient = Axios.create();
+export function setAuthorizationHeader(services: ServiceRepository, token: string) {
+ services.apiClient.defaults.headers.common = {
+ Authorization: `Bearer ${token}`
+ };
+ services.webdavClient.defaults.headers = {
+ Authorization: `Bearer ${token}`
+ };
+}
+
+export function removeAuthorizationHeader(services: ServiceRepository) {
+ delete services.apiClient.defaults.headers.common;
+ delete services.webdavClient.defaults.headers.common;
+}
+
+export const createServices = (config: Config, actions: ApiActions, useApiClient?: AxiosInstance) => {
+ // Need to give empty 'headers' object or it will create an
+ // instance with a reference to the global default headers object,
+ // which is very bad because that means setAuthorizationHeader
+ // would update the global default instead of the instance default.
+ const apiClient = useApiClient || Axios.create({ headers: {} });
apiClient.defaults.baseURL = config.baseUrl;
const webdavClient = new WebDAV();
constructor(serverApi: AxiosInstance, actions: ApiActions) {
super(serverApi, "users", actions);
}
+
+ activate(uuid: string) {
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .post(this.resourceType + `/${uuid}/activate`),
+ this.actions
+ );
+ }
+
+ unsetup(uuid: string) {
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .post(this.resourceType + `/${uuid}/unsetup`),
+ this.actions
+ );
+ }
}
"container_count_max": ${stringify(containerCountMax)},
"mounts": ${stringifyObject(mounts)},
"runtime_constraints": ${stringifyObject(runtimeConstraints)},
-"container_image": "${stringify(containerImage)}",
+"container_image": ${stringify(containerImage)},
"environment": ${stringifyObject(environment)},
"cwd": ${stringify(cwd)},
"command": ${stringifyObject(command)},
const collectionApiResponse = (apiResponse: CollectionResource) => {
const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, properties, portableDataHash, replicationDesired,
replicationConfirmedAt, replicationConfirmed, manifestText, deleteAt, trashAt, isTrashed, storageClassesDesired,
- storageClassesConfirmed, storageClassesConfirmedAt, currentVersionUuid, version, preserveVersion } = apiResponse;
+ storageClassesConfirmed, storageClassesConfirmedAt, currentVersionUuid, version, preserveVersion, fileCount, fileSizeTotal } = apiResponse;
const response = `
"uuid": "${uuid}",
"owner_uuid": "${ownerUuid}",
"storage_classes_desired": ${JSON.stringify(storageClassesDesired, null, 2)},
"storage_classes_confirmed": ${JSON.stringify(storageClassesConfirmed, null, 2)},
"storage_classes_confirmed_at": ${stringify(storageClassesConfirmedAt)},
-"currentVersionUuid": ${stringify(currentVersionUuid)},
+"current_version_uuid": ${stringify(currentVersionUuid)},
"version": ${version},
-"preserveVersion": ${preserveVersion}`;
+"preserve_version": ${preserveVersion},
+"file_count": ${fileCount},
+"file_size_total": ${fileSizeTotal}`;
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
const groupRequestApiResponse = (apiResponse: ProjectResource) => {
- const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, groupClass, trashAt, isTrashed, deleteAt, properties, writeableBy } = apiResponse;
+ const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, groupClass, trashAt, isTrashed, deleteAt, properties, writableBy } = apiResponse;
const response = `
"uuid": "${uuid}",
"owner_uuid": "${ownerUuid}",
"is_trashed": ${stringify(isTrashed)},
"delete_at": ${stringify(deleteAt)},
"properties": ${stringifyObject(properties)},
-"witable_by": ${stringifyObject(writeableBy)}`;
+"writable_by": ${stringifyObject(writableBy)}`;
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
import { Dispatch } from "redux";
import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
import { RootState } from "~/store/store";
-import { ServiceRepository } from "~/services/services";
+import { ServiceRepository, createServices, setAuthorizationHeader } from "~/services/services";
import Axios from "axios";
import { getUserFullname, User } from "~/models/user";
import { authActions } from "~/store/auth/auth-action";
-import { Config, DISCOVERY_URL } from "~/common/config";
+import {
+ Config, ClusterConfigJSON, CLUSTER_CONFIG_PATH, DISCOVERY_DOC_PATH,
+ buildConfig, mockClusterConfigJSON
+} from "~/common/config";
+import { normalizeURLPath } from "~/common/url";
import { Session, SessionStatus } from "~/models/session";
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
-import { AuthService, UserDetailsResponse } from "~/services/auth-service/auth-service";
+import { AuthService } from "~/services/auth-service/auth-service";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import * as jsSHA from "jssha";
-const getRemoteHostBaseUrl = async (remoteHost: string): Promise<string | null> => {
+const getClusterConfig = async (origin: string): Promise<Config | null> => {
+ // Try the new public config endpoint
+ try {
+ const config = (await Axios.get<ClusterConfigJSON>(`${origin}/${CLUSTER_CONFIG_PATH}`)).data;
+ return buildConfig(config);
+ } catch { }
+
+ // Fall back to discovery document
+ try {
+ const config = (await Axios.get<any>(`${origin}/${DISCOVERY_DOC_PATH}`)).data;
+ return {
+ baseUrl: normalizeURLPath(config.baseUrl),
+ keepWebServiceUrl: config.keepWebServiceUrl,
+ remoteHosts: config.remoteHosts,
+ rootUrl: config.rootUrl,
+ uuidPrefix: config.uuidPrefix,
+ websocketUrl: config.websocketUrl,
+ workbenchUrl: config.workbenchUrl,
+ workbench2Url: config.workbench2Url,
+ loginCluster: "",
+ vocabularyUrl: "",
+ fileViewersConfigUrl: "",
+ clusterConfig: mockClusterConfigJSON({})
+ };
+ } catch { }
+
+ return null;
+};
+
+const getRemoteHostConfig = async (remoteHost: string): Promise<Config | null> => {
let url = remoteHost;
if (url.indexOf('://') < 0) {
url = 'https://' + url;
}
const origin = new URL(url).origin;
- let baseUrl: string | null = null;
-
- try {
- const resp = await Axios.get<Config>(`${origin}/${DISCOVERY_URL}`);
- baseUrl = resp.data.baseUrl;
- } catch (err) {
- try {
- const resp = await Axios.get<any>(`${origin}/status.json`);
- baseUrl = resp.data.apiBaseURL;
- } catch (err) {
- }
- }
- if (baseUrl && baseUrl[baseUrl.length - 1] === '/') {
- baseUrl = baseUrl.substr(0, baseUrl.length - 1);
+ // Maybe it is an API server URL, try fetching config and discovery doc
+ let r = await getClusterConfig(origin);
+ if (r !== null) {
+ return r;
}
- return baseUrl;
-};
-
-const getUserDetails = async (baseUrl: string, token: string): Promise<UserDetailsResponse> => {
- const resp = await Axios.get<UserDetailsResponse>(`${baseUrl}/users/current`, {
- headers: {
- Authorization: `OAuth2 ${token}`
+ // Maybe it is a Workbench2 URL, try getting config.json
+ try {
+ r = await getClusterConfig((await Axios.get<any>(`${origin}/config.json`)).data.API_HOST);
+ if (r !== null) {
+ return r;
}
- });
- return resp.data;
-};
-
-const getTokenUuid = async (baseUrl: string, token: string): Promise<string> => {
- if (token.startsWith("v2/")) {
- const uuid = token.split("/")[1];
- return Promise.resolve(uuid);
- }
+ } catch { }
- const resp = await Axios.get(`${baseUrl}api_client_authorizations`, {
- headers: {
- Authorization: `OAuth2 ${token}`
- },
- data: {
- filters: JSON.stringify([['api_token', '=', token]])
+ // Maybe it is a Workbench1 URL, try getting status.json
+ try {
+ r = await getClusterConfig((await Axios.get<any>(`${origin}/status.json`)).data.apiBaseURL);
+ if (r !== null) {
+ return r;
}
- });
+ } catch { }
- return resp.data.items[0].uuid;
+ return null;
};
-export const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
+const invalidV2Token = "Must be a v2 token";
+
+export const getSaltedToken = (clusterId: string, token: string) => {
const shaObj = new jsSHA("SHA-1", "TEXT");
- let secret = token;
- if (token.startsWith("v2/")) {
- secret = token.split("/")[2];
+ const [ver, uuid, secret] = token.split("/");
+ if (ver !== "v2") {
+ throw new Error(invalidV2Token);
}
- shaObj.setHMACKey(secret, "TEXT");
- shaObj.update(clusterId);
- const hmac = shaObj.getHMAC("HEX");
- return `v2/${tokenUuid}/${hmac}`;
-};
-
-const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{ user: User, token: string }> => {
- const tokenUuid = await getTokenUuid(activeSession.baseUrl, activeSession.token);
- const saltedToken = getSaltedToken(clusterId, tokenUuid, activeSession.token);
- const user = await getUserDetails(baseUrl, saltedToken);
- return {
- user: {
- firstName: user.first_name,
- lastName: user.last_name,
- uuid: user.uuid,
- ownerUuid: user.owner_uuid,
- email: user.email,
- isAdmin: user.is_admin,
- isActive: user.is_active,
- username: user.username,
- prefs: user.prefs
- },
- token: saltedToken
- };
+ let salted = secret;
+ if (uuid.substr(0, 5) !== clusterId) {
+ shaObj.setHMACKey(secret, "TEXT");
+ shaObj.update(clusterId);
+ salted = shaObj.getHMAC("HEX");
+ }
+ return `v2/${uuid}/${salted}`;
};
export const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
-export const validateCluster = async (remoteHost: string, clusterId: string, activeSession: Session): Promise<{ user: User; token: string, baseUrl: string }> => {
- const baseUrl = await getRemoteHostBaseUrl(remoteHost);
- if (!baseUrl) {
- return Promise.reject(`Could not find base url for ${remoteHost}`);
- }
- const { user, token } = await clusterLogin(clusterId, baseUrl, activeSession);
- return { baseUrl, user, token };
+export const validateCluster = async (config: Config, useToken: string):
+ Promise<{ user: User; token: string }> => {
+
+ const saltedToken = getSaltedToken(config.uuidPrefix, useToken);
+
+ const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
+ setAuthorizationHeader(svc, saltedToken);
+
+ const user = await svc.authService.getUserDetails();
+ return {
+ user,
+ token: saltedToken,
+ };
};
export const validateSession = (session: Session, activeSession: Session) =>
async (dispatch: Dispatch): Promise<Session> => {
dispatch(authActions.UPDATE_SESSION({ ...session, status: SessionStatus.BEING_VALIDATED }));
session.loggedIn = false;
- try {
- const { baseUrl, user, token } = await validateCluster(session.remoteHost, session.clusterId, activeSession);
+
+ const setupSession = (baseUrl: string, user: User, token: string) => {
session.baseUrl = baseUrl;
session.token = token;
session.email = user.email;
- session.username = getUserFullname(user);
+ session.uuid = user.uuid;
+ session.name = getUserFullname(user);
session.loggedIn = true;
- } catch {
- session.loggedIn = false;
- } finally {
- session.status = SessionStatus.VALIDATED;
- dispatch(authActions.UPDATE_SESSION(session));
+ };
+
+ let fail: Error | null = null;
+ const config = await getRemoteHostConfig(session.remoteHost);
+ if (config !== null) {
+ dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config }));
+ try {
+ const { user, token } = await validateCluster(config, session.token);
+ setupSession(config.baseUrl, user, token);
+ } catch (e) {
+ fail = new Error(`Getting current user for ${session.remoteHost}: ${e.message}`);
+ try {
+ const { user, token } = await validateCluster(config, activeSession.token);
+ setupSession(config.baseUrl, user, token);
+ fail = null;
+ } catch (e2) {
+ if (e.message === invalidV2Token) {
+ fail = new Error(`Getting current user for ${session.remoteHost}: ${e2.message}`);
+ }
+ }
+ }
+ } else {
+ fail = new Error(`Could not get config for ${session.remoteHost}`);
}
+ session.status = SessionStatus.VALIDATED;
+ dispatch(authActions.UPDATE_SESSION(session));
+
+ if (fail) {
+ throw fail;
+ }
+
return session;
};
dispatch(progressIndicatorActions.START_WORKING("sessionsValidation"));
for (const session of sessions) {
if (session.status === SessionStatus.INVALIDATED) {
- await dispatch(validateSession(session, activeSession));
+ try {
+ /* Here we are dispatching a function, not an
+ action. This is legal (it calls the
+ function with a 'Dispatch' object as the
+ first parameter) but the typescript
+ annotations don't understand this case, so
+ we get an error from typescript unless
+ override it using Dispatch<any>. This
+ pattern is used in a bunch of different
+ places in Workbench2. */
+ await dispatch(validateSession(session, activeSession));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ kind: SnackbarKind.ERROR
+ }));
+ }
}
}
- services.authService.saveSessions(sessions);
+ services.authService.saveSessions(getState().auth.sessions);
dispatch(progressIndicatorActions.STOP_WORKING("sessionsValidation"));
}
};
-export const addSession = (remoteHost: string) =>
+export const addSession = (remoteHost: string, token?: string, sendToLogin?: boolean) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const sessions = getState().auth.sessions;
const activeSession = getActiveSession(sessions);
- if (activeSession) {
- const clusterId = remoteHost.match(/^(\w+)\./)![1];
- if (sessions.find(s => s.clusterId === clusterId)) {
- return Promise.reject("Cluster already exists");
+ let useToken: string | null = null;
+ if (token) {
+ useToken = token;
+ } else if (activeSession) {
+ useToken = activeSession.token;
+ }
+
+ if (useToken) {
+ const config = await getRemoteHostConfig(remoteHost);
+ if (!config) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: `Could not get config for ${remoteHost}`,
+ kind: SnackbarKind.ERROR
+ }));
+ return;
}
+
try {
- const { baseUrl, user, token } = await validateCluster(remoteHost, clusterId, activeSession);
+ dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config }));
+ const { user, token } = await validateCluster(config, useToken);
const session = {
loggedIn: true,
status: SessionStatus.VALIDATED,
active: false,
email: user.email,
- username: getUserFullname(user),
+ name: getUserFullname(user),
+ uuid: user.uuid,
+ baseUrl: config.baseUrl,
+ clusterId: config.uuidPrefix,
remoteHost,
- baseUrl,
- clusterId,
token
};
- dispatch(authActions.ADD_SESSION(session));
+ if (sessions.find(s => s.clusterId === config.uuidPrefix)) {
+ await dispatch(authActions.UPDATE_SESSION(session));
+ } else {
+ await dispatch(authActions.ADD_SESSION(session));
+ }
services.authService.saveSessions(getState().auth.sessions);
return session;
- } catch (e) {
+ } catch {
+ if (sendToLogin) {
+ const rootUrl = new URL(config.baseUrl);
+ rootUrl.pathname = "";
+ window.location.href = `${rootUrl.toString()}/login?return_to=` + encodeURI(`${window.location.protocol}//${window.location.host}/add-session?baseURL=` + encodeURI(rootUrl.toString()));
+ return;
+ }
}
}
- return Promise.reject("Could not validate cluster");
+ return Promise.reject(new Error("Could not validate cluster"));
};
-export const toggleSession = (session: Session) =>
+
+export const removeSession = (clusterId: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- let s = { ...session };
+ await dispatch(authActions.REMOVE_SESSION(clusterId));
+ services.authService.saveSessions(getState().auth.sessions);
+ };
+
+export const toggleSession = (session: Session) =>
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const s: Session = { ...session };
if (session.loggedIn) {
s.loggedIn = false;
+ dispatch(authActions.UPDATE_SESSION(s));
} else {
const sessions = getState().auth.sessions;
const activeSession = getActiveSession(sessions);
if (activeSession) {
- s = await dispatch<any>(validateSession(s, activeSession)) as Session;
+ try {
+ await dispatch(validateSession(s, activeSession));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ kind: SnackbarKind.ERROR
+ }));
+ s.loggedIn = false;
+ dispatch(authActions.UPDATE_SESSION(s));
+ }
}
}
- dispatch(authActions.UPDATE_SESSION(s));
services.authService.saveSessions(getState().auth.sessions);
};
export const initSessions = (authService: AuthService, config: Config, user: User) =>
(dispatch: Dispatch<any>) => {
const sessions = authService.buildSessions(config, user);
- authService.saveSessions(sessions);
dispatch(authActions.SET_SESSIONS(sessions));
+ dispatch(validateSessions());
};
export const loadSiteManagerPanel = () =>
import { dialogActions } from "~/store/dialog/dialog-actions";
import { Dispatch } from "redux";
import { RootState } from "~/store/store";
+import { getUserUuid } from "~/common/getuser";
import { ServiceRepository } from "~/services/services";
-import {snackbarActions, SnackbarKind} from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import { FormErrors, reset, startSubmit, stopSubmit } from "redux-form";
import { KeyType } from "~/models/ssh-key";
import {
getAuthorizedKeysServiceError
} from "~/services/authorized-keys-service/authorized-keys-service";
import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
-import {
- authActions,
-} from "~/store/auth/auth-action";
+import { authActions } from "~/store/auth/auth-action";
export const SSH_KEY_CREATE_FORM_NAME = 'sshKeyCreateFormName';
export const SSH_KEY_PUBLIC_KEY_DIALOG = 'sshKeyPublicKeyDialog';
export const createSshKey = (data: SshKeyCreateFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = getState().auth.user!.uuid;
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
const { name, publicKey } = data;
dispatch(startSubmit(SSH_KEY_CREATE_FORM_NAME));
try {
export const loadSshKeysPanel = () =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
try {
- dispatch(setBreadcrumbs([{ label: 'SSH Keys'}]));
+ dispatch(setBreadcrumbs([{ label: 'SSH Keys' }]));
const response = await services.authorizedKeysService.list();
dispatch(authActions.SET_SSH_KEYS(response.items));
} catch (e) {
return;
}
};
-
//
// SPDX-License-Identifier: AGPL-3.0
-import { authReducer, AuthState } from "./auth-reducer";
-import { AuthAction, initAuth } from "./auth-action";
-import {
- API_TOKEN_KEY,
- USER_EMAIL_KEY,
- USER_FIRST_NAME_KEY,
- USER_LAST_NAME_KEY,
- USER_OWNER_UUID_KEY,
- USER_UUID_KEY,
- USER_IS_ADMIN,
- USER_IS_ACTIVE,
- USER_USERNAME,
- USER_PREFS
-} from "~/services/auth-service/auth-service";
+import { initAuth } from "./auth-action";
+import { API_TOKEN_KEY } from "~/services/auth-service/auth-service";
import 'jest-localstorage-mock';
-import { createServices } from "~/services/services";
+import { ServiceRepository, createServices } from "~/services/services";
import { configureStore, RootStore } from "../store";
import createBrowserHistory from "history/createBrowserHistory";
-import { Config, mockConfig } from '~/common/config';
+import { mockConfig } from '~/common/config';
import { ApiActions } from "~/services/api/api-actions";
-import { ACCOUNT_LINK_STATUS_KEY} from '~/services/link-account-service/link-account-service';
+import { ACCOUNT_LINK_STATUS_KEY } from '~/services/link-account-service/link-account-service';
+import Axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import { ImportMock } from 'ts-mock-imports';
+import * as servicesModule from "~/services/services";
describe('auth-actions', () => {
- let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+ const axiosInst = Axios.create({ headers: {} });
+ const axiosMock = new MockAdapter(axiosInst);
+
let store: RootStore;
+ let services: ServiceRepository;
const actions: ApiActions = {
progressFn: (id: string, working: boolean) => { },
errorFn: (id: string, message: string) => { }
};
+ let importMocks: any[];
beforeEach(() => {
- store = configureStore(createBrowserHistory(), createServices(mockConfig({}), actions));
+ axiosMock.reset();
+ services = createServices(mockConfig({}), actions, axiosInst);
+ store = configureStore(createBrowserHistory(), services);
localStorage.clear();
- reducer = authReducer(createServices(mockConfig({}), actions));
+ importMocks = [];
+ });
+
+ afterEach(() => {
+ importMocks.map(m => m.restore());
});
- it('should initialise state with user and api token from local storage', () => {
+ it('should initialise state with user and api token from local storage', (done) => {
+
+ axiosMock
+ .onGet("/users/current")
+ .reply(200, {
+ email: "test@test.com",
+ first_name: "John",
+ last_name: "Doe",
+ uuid: "zzzzz-tpzed-abcefg",
+ owner_uuid: "ownerUuid",
+ is_admin: false,
+ is_active: true,
+ username: "jdoe",
+ prefs: {}
+ });
+
+ importMocks.push(ImportMock.mockFunction(servicesModule, 'createServices', services));
// Only test the case when a link account operation is not being cancelled
sessionStorage.setItem(ACCOUNT_LINK_STATUS_KEY, "0");
localStorage.setItem(API_TOKEN_KEY, "token");
- localStorage.setItem(USER_EMAIL_KEY, "test@test.com");
- localStorage.setItem(USER_FIRST_NAME_KEY, "John");
- localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
- localStorage.setItem(USER_UUID_KEY, "zzzzz-tpzed-abcefg");
- localStorage.setItem(USER_USERNAME, "username");
- localStorage.setItem(USER_PREFS, JSON.stringify({}));
- localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
- localStorage.setItem(USER_IS_ADMIN, JSON.stringify(false));
- localStorage.setItem(USER_IS_ACTIVE, JSON.stringify(true));
const config: any = {
rootUrl: "https://zzzzz.arvadosapi.com",
store.dispatch(initAuth(config));
- expect(store.getState().auth).toEqual({
- apiToken: "token",
- sshKeys: [],
- homeCluster: "zzzzz",
- localCluster: "zzzzz",
- remoteHostsConfig: {},
- remoteHosts: {
- zzzzz: "zzzzz.arvadosapi.com",
- xc59z: "xc59z.arvadosapi.com"
- },
- sessions: [{
- "active": true,
- "baseUrl": undefined,
- "clusterId": "zzzzz",
- "email": "test@test.com",
- "loggedIn": true,
- "remoteHost": "https://zzzzz.arvadosapi.com",
- "status": 2,
- "token": "token",
- "username": "John Doe"
- }, {
- "active": false,
- "baseUrl": "",
- "clusterId": "xc59z",
- "email": "",
- "loggedIn": false,
- "remoteHost": "xc59z.arvadosapi.com",
- "status": 0,
- "token": "",
- "username": ""
- }],
- user: {
- email: "test@test.com",
- firstName: "John",
- lastName: "Doe",
- uuid: "zzzzz-tpzed-abcefg",
- ownerUuid: "ownerUuid",
- username: "username",
- prefs: {},
- isAdmin: false,
- isActive: true
+ store.subscribe(() => {
+ const auth = store.getState().auth;
+ if (auth.apiToken === "token" &&
+ auth.sessions.length === 2 &&
+ auth.sessions[0].status === 2 &&
+ auth.sessions[1].status === 2
+ ) {
+ try {
+ expect(auth).toEqual({
+ apiToken: "token",
+ config: {
+ remoteHosts: {
+ "xc59z": "xc59z.arvadosapi.com",
+ },
+ rootUrl: "https://zzzzz.arvadosapi.com",
+ uuidPrefix: "zzzzz",
+ },
+ sshKeys: [],
+ homeCluster: "zzzzz",
+ localCluster: "zzzzz",
+ loginCluster: undefined,
+ remoteHostsConfig: {
+ "zzzzz": {
+ "remoteHosts": {
+ "xc59z": "xc59z.arvadosapi.com",
+ },
+ "rootUrl": "https://zzzzz.arvadosapi.com",
+ "uuidPrefix": "zzzzz",
+ },
+ },
+ remoteHosts: {
+ zzzzz: "zzzzz.arvadosapi.com",
+ xc59z: "xc59z.arvadosapi.com"
+ },
+ sessions: [{
+ "active": true,
+ "baseUrl": undefined,
+ "clusterId": "zzzzz",
+ "email": "test@test.com",
+ "loggedIn": true,
+ "remoteHost": "https://zzzzz.arvadosapi.com",
+ "status": 2,
+ "token": "token",
+ "name": "John Doe"
+ "uuid": "zzzzz-tpzed-abcefg",
+ }, {
+ "active": false,
+ "baseUrl": "",
+ "clusterId": "xc59z",
+ "email": "",
+ "loggedIn": false,
+ "remoteHost": "xc59z.arvadosapi.com",
+ "status": 2,
+ "token": "",
+ "name": "",
+ "uuid": "",
+ }],
+ user: {
+ email: "test@test.com",
+ firstName: "John",
+ lastName: "Doe",
+ uuid: "zzzzz-tpzed-abcefg",
+ ownerUuid: "ownerUuid",
+ username: "jdoe",
+ prefs: { profile: {} },
+ isAdmin: false,
+ isActive: true
+ }
+ });
+ done();
+ } catch (e) {
+ console.log(e);
+ }
}
});
});
+
// TODO: Add remaining action tests
/*
it('should fire external url to login', () => {
import { ofType, unionize, UnionOf } from '~/common/unionize';
import { Dispatch } from "redux";
-import { AxiosInstance } from "axios";
import { RootState } from "../store";
import { ServiceRepository } from "~/services/services";
import { SshKeyResource } from '~/models/ssh-key';
-import { User, UserResource } from "~/models/user";
+import { User } from "~/models/user";
import { Session } from "~/models/session";
-import { getDiscoveryURL, Config } from '~/common/config';
-import { initSessions } from "~/store/auth/auth-action-session";
-import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
+import { Config } from '~/common/config';
import { matchTokenRoute, matchFedTokenRoute } from '~/routes/routes';
-import Axios from "axios";
-import { AxiosError } from "axios";
+import { createServices, setAuthorizationHeader } from "~/services/services";
+import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
export const authActions = unionize({
- SAVE_API_TOKEN: ofType<string>(),
- SAVE_USER: ofType<UserResource>(),
LOGIN: {},
- LOGOUT: {},
- CONFIG: ofType<{ config: Config }>(),
- INIT: ofType<{ user: User, token: string }>(),
+ LOGOUT: ofType<{ deleteLinkData: boolean }>(),
+ SET_CONFIG: ofType<{ config: Config }>(),
+ INIT_USER: ofType<{ user: User, token: string }>(),
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>(),
SET_SSH_KEYS: ofType<SshKeyResource[]>(),
REMOTE_CLUSTER_CONFIG: ofType<{ config: Config }>(),
});
-export function setAuthorizationHeader(services: ServiceRepository, token: string) {
- services.apiClient.defaults.headers.common = {
- Authorization: `OAuth2 ${token}`
- };
- services.webdavClient.defaults.headers = {
- Authorization: `OAuth2 ${token}`
- };
-}
-
-function removeAuthorizationHeader(client: AxiosInstance) {
- delete client.defaults.headers.common.Authorization;
-}
-
export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
// Cancel any link account ops in progress unless the user has
// just logged in or there has been a successful link operation
};
const init = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const user = services.authService.getUser();
const token = services.authService.getApiToken();
- const homeCluster = services.authService.getHomeCluster();
- if (token) {
- setAuthorizationHeader(services, token);
+ let homeCluster = services.authService.getHomeCluster();
+ if (homeCluster && !config.remoteHosts[homeCluster]) {
+ homeCluster = undefined;
}
- dispatch(authActions.CONFIG({ config }));
- dispatch(authActions.SET_HOME_CLUSTER(homeCluster || config.uuidPrefix));
- if (token && user) {
- dispatch(authActions.INIT({ user, token }));
- dispatch<any>(initSessions(services.authService, config, user));
- dispatch<any>(getUserDetails()).then((user: User) => {
- dispatch(authActions.INIT({ user, token }));
- }).catch((err: AxiosError) => {
- if (err.response) {
- // Bad token
- if (err.response.status === 401) {
- logout()(dispatch, getState, services);
- }
- }
+ dispatch(authActions.SET_CONFIG({ config }));
+ dispatch(authActions.SET_HOME_CLUSTER(config.loginCluster || homeCluster || config.uuidPrefix));
+
+ if (token && token !== "undefined") {
+ dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
+ dispatch<any>(saveApiToken(token)).then(() => {
+ dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+ }).catch(() => {
+ dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
});
}
- Object.keys(config.remoteHosts).map((k) => {
- Axios.get<Config>(getDiscoveryURL(config.remoteHosts[k]))
- .then(response => dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config: response.data })));
- });
};
-export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- services.authService.saveApiToken(token);
- setAuthorizationHeader(services, token);
- dispatch(authActions.SAVE_API_TOKEN(token));
+export const getConfig = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Config => {
+ const state = getState().auth;
+ return state.remoteHostsConfig[state.localCluster];
};
-export const saveUser = (user: UserResource) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- services.authService.saveUser(user);
- dispatch(authActions.SAVE_USER(user));
+export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ const config = dispatch<any>(getConfig);
+ const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
+ setAuthorizationHeader(svc, token);
+ return svc.authService.getUserDetails().then((user: User) => {
+ dispatch(authActions.INIT_USER({ user, token }));
+ });
};
-export const login = (uuidPrefix: string, homeCluster: string, remoteHosts: { [key: string]: string }) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- services.authService.login(uuidPrefix, homeCluster, remoteHosts);
- dispatch(authActions.LOGIN());
-};
+export const login = (uuidPrefix: string, homeCluster: string, loginCluster: string,
+ remoteHosts: { [key: string]: string }) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ services.authService.login(uuidPrefix, homeCluster, loginCluster, remoteHosts);
+ dispatch(authActions.LOGIN());
+ };
export const logout = (deleteLinkData: boolean = false) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- if (deleteLinkData) {
- services.linkAccountService.removeAccountToLink();
- }
- services.authService.removeApiToken();
- services.authService.removeUser();
- removeAuthorizationHeader(services.apiClient);
- services.authService.logout();
- dispatch(authActions.LOGOUT());
-};
-
-export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<User> => {
- dispatch(authActions.USER_DETAILS_REQUEST());
- return services.authService.getUserDetails().then(user => {
- services.authService.saveUser(user);
- dispatch(authActions.USER_DETAILS_SUCCESS(user));
- return user;
- });
+ dispatch(authActions.LOGOUT({ deleteLinkData }));
};
export type AuthAction = UnionOf<typeof authActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Middleware } from "redux";
+import { authActions, } from "./auth-action";
+import { ServiceRepository, setAuthorizationHeader, removeAuthorizationHeader } from "~/services/services";
+import { initSessions } from "~/store/auth/auth-action-session";
+import { User } from "~/models/user";
+import { RootState } from '~/store/store';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
+
+export const authMiddleware = (services: ServiceRepository): Middleware => store => next => action => {
+ // Middleware to update external state (local storage, window
+ // title) to ensure that they stay in sync with redux state.
+
+ authActions.match(action, {
+ INIT_USER: ({ user, token }) => {
+ // The "next" method passes the action to the next
+ // middleware in the chain, or the reducer. That means
+ // after next() returns, the action has (presumably) been
+ // applied by the reducer to update the state.
+ next(action);
+
+ const state: RootState = store.getState();
+
+ if (state.auth.apiToken) {
+ services.authService.saveApiToken(state.auth.apiToken);
+ setAuthorizationHeader(services, state.auth.apiToken);
+ } else {
+ services.authService.removeApiToken();
+ removeAuthorizationHeader(services);
+ }
+
+ store.dispatch<any>(initSessions(services.authService, state.auth.remoteHostsConfig[state.auth.localCluster], user));
+ if (!user.isActive) {
+ // As a special case, if the user is inactive, they
+ // may be able to self-activate using the "activate"
+ // method. Note, for this to work there can't be any
+ // unsigned user agreements, we assume the API server is just going to
+ // rubber-stamp our activation request. At some point in the future we'll
+ // want to either add support for displaying/signing user
+ // agreements or get rid of self-activation.
+ // For more details, see:
+ // https://doc.arvados.org/master/admin/user-management.html
+
+ store.dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
+ services.userService.activate(user.uuid).then((user: User) => {
+ store.dispatch(authActions.INIT_USER({ user, token }));
+ store.dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+ }).catch(() => {
+ store.dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+ });
+ }
+ },
+ SET_CONFIG: ({ config }) => {
+ document.title = `Arvados Workbench (${config.uuidPrefix})`;
+ next(action);
+ },
+ LOGOUT: ({ deleteLinkData }) => {
+ next(action);
+ if (deleteLinkData) {
+ services.linkAccountService.removeAccountToLink();
+ }
+ services.authService.removeApiToken();
+ services.authService.removeUser();
+ removeAuthorizationHeader(services);
+ services.authService.logout();
+ },
+ default: () => next(action)
+ });
+};
isAdmin: false,
isActive: true
};
- const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
+ const state = reducer(initialState, authActions.INIT_USER({ user, token: "token" }));
expect(state).toEqual({
apiToken: "token",
+ config: mockConfig({}),
user,
sshKeys: [],
sessions: [],
homeCluster: "zzzzz",
localCluster: "",
- remoteHosts: {},
- remoteHostsConfig: {}
- });
- });
-
- it('should save api token', () => {
- const initialState = undefined;
-
- const state = reducer(initialState, authActions.SAVE_API_TOKEN("token"));
- expect(state).toEqual({
- apiToken: "token",
- user: undefined,
- sshKeys: [],
- sessions: [],
- homeCluster: "",
- localCluster: "",
+ loginCluster: "",
remoteHosts: {},
remoteHostsConfig: {}
});
const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
expect(state).toEqual({
apiToken: undefined,
+ config: mockConfig({}),
sshKeys: [],
sessions: [],
- homeCluster: "",
+ homeCluster: "uuid",
localCluster: "",
+ loginCluster: "",
remoteHosts: {},
remoteHostsConfig: {},
user: {
// SPDX-License-Identifier: AGPL-3.0
import { authActions, AuthAction } from "./auth-action";
-import { User, UserResource } from "~/models/user";
+import { User } from "~/models/user";
import { ServiceRepository } from "~/services/services";
import { SshKeyResource } from '~/models/ssh-key';
import { Session } from "~/models/session";
-import { Config } from '~/common/config';
+import { Config, mockConfig } from '~/common/config';
export interface AuthState {
user?: User;
sessions: Session[];
localCluster: string;
homeCluster: string;
+ loginCluster: string;
remoteHosts: { [key: string]: string };
remoteHostsConfig: { [key: string]: Config };
+ config: Config;
}
const initialState: AuthState = {
sessions: [],
localCluster: "",
homeCluster: "",
+ loginCluster: "",
remoteHosts: {},
- remoteHostsConfig: {}
+ remoteHostsConfig: {},
+ config: mockConfig({})
};
export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => {
return authActions.match(action, {
- SAVE_API_TOKEN: (token: string) => {
- return { ...state, apiToken: token };
- },
- SAVE_USER: (user: UserResource) => {
- return { ...state, user};
- },
- CONFIG: ({ config }) => {
+ SET_CONFIG: ({ config }) => {
return {
...state,
+ config,
localCluster: config.uuidPrefix,
remoteHosts: { ...config.remoteHosts, [config.uuidPrefix]: new URL(config.rootUrl).host },
- homeCluster: config.uuidPrefix
+ homeCluster: config.loginCluster || config.uuidPrefix,
+ loginCluster: config.loginCluster,
+ remoteHostsConfig: { ...state.remoteHostsConfig, [config.uuidPrefix]: config }
};
},
REMOTE_CLUSTER_CONFIG: ({ config }) => {
remoteHostsConfig: { ...state.remoteHostsConfig, [config.uuidPrefix]: config },
};
},
- INIT: ({ user, token }) => {
+ INIT_USER: ({ user, token }) => {
return { ...state, user, apiToken: token, homeCluster: user.uuid.substr(0, 5) };
},
LOGIN: () => {
return { ...state, apiToken: undefined };
},
USER_DETAILS_SUCCESS: (user: User) => {
- return { ...state, user };
+ return { ...state, user, homeCluster: user.uuid.substr(0, 5) };
},
SET_SSH_KEYS: (sshKeys: SshKeyResource[]) => {
return { ...state, sshKeys };
import { Dispatch } from 'redux';
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
import { getResource } from '~/store/resources/resources';
import { TreePicker } from '../tree-picker/tree-picker';
const path = getState().router.location!.pathname;
const currentUuid = path.split('/')[2];
const uuidKind = extractUuidKind(currentUuid);
-
+
if (uuidKind === ResourceKind.COLLECTION) {
const collectionItem = await services.collectionService.get(currentUuid);
dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
export const setProjectBreadcrumbs = (uuid: string) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const ancestors = getSidePanelTreeNodeAncestorsIds(uuid)(getState().treePicker);
- const rootUuid = services.authService.getUuid();
+ const rootUuid = getUserUuid(getState());
if (uuid === rootUuid || ancestors.find(uuid => uuid === rootUuid)) {
dispatch(setSidePanelBreadcrumbs(uuid));
} else {
const uuid = item ? item.uuid : '';
try {
if (item) {
- item.properties[data.key] = data.value;
- const version = 'version';
- delete item[version];
- const updatedCollection = await services.collectionService.update(uuid, item);
+ const updatedCollection = await services.collectionService.update(
+ uuid, {
+ properties: {
+ ...JSON.parse(JSON.stringify(item.properties)),
+ [data.keyID || data.key]: data.valueID || data.value
+ }
+ }
+ );
+ item.properties = updatedCollection.properties;
dispatch(resourcesActions.SET_RESOURCES([updatedCollection]));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Tag has been successfully added.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
return updatedCollection;
}
return;
} catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.errors[0], hideDuration: 2000, kind: SnackbarKind.ERROR }));
return;
}
};
await services.containerRequestService.get(uuid);
dispatch<any>(navigateTo(uuid));
} catch {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'This process does not exists!', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'This process does not exist!', hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
};
try {
if (item) {
delete item.properties[key];
- const updatedCollection = await services.collectionService.update(uuid, item);
+ const updatedCollection = await services.collectionService.update(
+ uuid, {
+ properties: {...item.properties}
+ }
+ );
dispatch(resourcesActions.SET_RESOURCES([updatedCollection]));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Tag has been successfully deleted.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
return updatedCollection;
}
return;
} catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.errors[0], hideDuration: 2000, kind: SnackbarKind.ERROR }));
return;
}
};
import { unionize, ofType, UnionOf } from "~/common/unionize";
import { Dispatch } from "redux";
-import { CollectionFilesTree, CollectionFileType } from "~/models/collection-file";
+import { CollectionFilesTree, CollectionFileType, createCollectionFilesTree } from "~/models/collection-file";
import { ServiceRepository } from "~/services/services";
import { RootState } from "../../store";
import { snackbarActions, SnackbarKind } from "../../snackbar/snackbar-actions";
import { dialogActions } from '../../dialog/dialog-actions';
-import { getNodeValue } from "~/models/tree";
+import { getNodeValue, mapTreeValues } from "~/models/tree";
import { filterCollectionFilesBySelection } from './collection-panel-files-state';
-import { startSubmit, stopSubmit, reset, initialize, FormErrors } from 'redux-form';
+import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
import { getDialog } from "~/store/dialog/dialog-reducer";
-import { getFileFullPath } from "~/services/collection-service/collection-service-files-response";
-import { resourcesDataActions } from "~/store/resources-data/resources-data-actions";
+import { getFileFullPath, sortFilesTree } from "~/services/collection-service/collection-service-files-response";
export const collectionPanelFilesAction = unionize({
SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
export const loadCollectionFiles = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const files = await services.collectionService.files(uuid);
- dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
- dispatch(resourcesDataActions.SET_FILES({ uuid, files }));
+
+ // Given the array of directories and files, create the appropriate tree nodes,
+ // sort them, and add the complete url to each.
+ const tree = createCollectionFilesTree(files);
+ const sorted = sortFilesTree(tree);
+ const mapped = mapTreeValues(services.collectionService.extendFileURL)(sorted);
+ dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(mapped));
};
export const removeCollectionFiles = (filePaths: string[]) =>
import { MiddlewareAPI, Dispatch } from 'redux';
import { DataExplorerMiddlewareService } from '~/store/data-explorer/data-explorer-middleware-service';
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
import { resourcesActions } from '~/store/resources/resources-actions';
}
try {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
- const userUuid = api.getState().auth.user!.uuid;
+ const userUuid = getUserUuid(api.getState());
const pathname = api.getState().router.location!.pathname;
const contentAddress = pathname.split('/')[2];
const response = await this.services.collectionService.list({
.addIn('uuid', groupUuids)
.getFilters()
});
- responseUsers.items.map(it=>{
- api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid}));
+ responseUsers.items.map(it => {
+ api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({ name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid }));
});
- responseGroups.items.map(it=>{
- api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: `Project: ${it.name}`, uuid: it.uuid}));
+ responseGroups.items.map(it => {
+ api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({ name: `Project: ${it.name}`, uuid: it.uuid }));
});
api.dispatch<any>(setBreadcrumbs([{ label: 'Projects', uuid: userUuid }]));
api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
snackbarActions.OPEN_SNACKBAR({
message: 'Could not fetch collection with this content address.',
kind: SnackbarKind.ERROR
- });
\ No newline at end of file
+ });
return newCollection;
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(
COLLECTION_COPY_FORM_NAME,
{ ownerUuid: 'A collection with the same name already exists in the target project.' } as FormErrors
import { Dispatch } from "redux";
import { reset, startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { dialogActions } from "~/store/dialog/dialog-actions";
import { ServiceRepository } from '~/services/services';
import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
const router = getState();
const properties = getState().properties;
if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
- const userUuid = getState().auth.user!.uuid;
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { userUuid }));
} else {
dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { ownerUuid }));
await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
dispatch(reset(COLLECTION_CREATE_FORM_NAME));
- dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
return newCollection;
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors));
} else if (error === CommonResourceServiceError.NONE) {
dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME));
}));
await services.collectionService.delete(newCollection!.uuid);
}
- dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
return;
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
}
};
dispatch(startSubmit(COLLECTION_MOVE_FORM_NAME));
try {
dispatch(progressIndicatorActions.START_WORKING(COLLECTION_MOVE_FORM_NAME));
+ await services.collectionService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
const collection = await services.collectionService.get(resource.uuid);
- await services.collectionService.update(resource.uuid, { ...collection, ownerUuid: resource.ownerUuid });
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
return collection;
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(COLLECTION_MOVE_FORM_NAME, { ownerUuid: 'A collection with the same name already exists in the target project.' } as FormErrors));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME, { name: 'Collection with this name already exists.' } as FormErrors));
} else if (error === CommonResourceServiceError.UNKNOWN) {
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const uuid = collection.uuid || '';
dispatch(startSubmit(COLLECTION_UPDATE_FORM_NAME));
+ dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
try {
- dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
const updatedCollection = await services.collectionService.update(uuid, collection);
dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
return updatedCollection;
} catch (e) {
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors));
+ } else {
+ // Unknown error, handling left to caller.
+ dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
+ throw(e);
}
- dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
- return;
}
+ return;
};
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ofType, unionize, UnionOf } from '~/common/unionize';
-import { Config } from '~/common/config';
-
-export const configActions = unionize({
- CONFIG: ofType<{ config: Config }>(),
-});
-
-export type ConfigAction = UnionOf<typeof configActions>;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { configActions, ConfigAction } from "./config-action";
-import { mockConfig } from '~/common/config';
-
-export const configReducer = (state = mockConfig({}), action: ConfigAction) => {
- return configActions.match(action, {
- CONFIG: ({ config }) => {
- return {
- ...state, ...config
- };
- },
- default: () => state
- });
-};
try {
if (project) {
delete project.properties[key];
- const updatedProject = await services.projectService.update(project.uuid, project);
+ const updatedProject = await services.projectService.update(project.uuid, { properties: project.properties });
dispatch(resourcesActions.SET_RESOURCES([updatedProject]));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully deleted.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
}
dispatch(startSubmit(PROJECT_PROPERTIES_FORM_NAME));
try {
if (project) {
- project.properties[data.key] = data.value;
- const updatedProject = await services.projectService.update(project.uuid, project);
+ const updatedProject = await services.projectService.update(
+ project.uuid, {
+ properties: {
+ ...JSON.parse(JSON.stringify(project.properties)),
+ [data.keyID || data.key]: data.valueID || data.value
+ }
+ }
+ );
dispatch(resourcesActions.SET_RESOURCES([updatedProject]));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully added.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
dispatch(stopSubmit(PROJECT_PROPERTIES_FORM_NAME));
import { DataExplorerMiddlewareService } from "~/store/data-explorer/data-explorer-middleware-service";
import { FavoritePanelColumnNames } from "~/views/favorite-panel/favorite-panel";
import { RootState } from "../store";
+import { getUserUuid } from "~/common/getuser";
import { DataColumns } from "~/components/data-table/data-table";
import { ServiceRepository } from "~/services/services";
import { SortDirection } from "~/components/data-table/data-column";
const responseLinks = await this.services.linkService.list({
filters: new FilterBuilder()
.addEqual("linkClass", 'star')
- .addEqual('tailUuid', this.services.authService.getUuid()!)
+ .addEqual('tailUuid', getUserUuid(api.getState()))
.addEqual('tailKind', ResourceKind.USER)
.getFilters()
}).then(results => results);
response.itemsAvailable++;
response.items.push(it);
});
-
+
api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch(resourcesActions.SET_RESOURCES(response.items));
await api.dispatch<any>(loadMissingProcessesInformation(response.items));
import { unionize, ofType, UnionOf } from "~/common/unionize";
import { Dispatch } from "redux";
import { RootState } from "../store";
+import { getUserUuid } from "~/common/getuser";
import { checkFavorite } from "./favorites-reducer";
import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
import { ServiceRepository } from "~/services/services";
export const toggleFavorite = (resource: { uuid: string; name: string }) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) {
+ return Promise.reject("No user");
+ }
dispatch(progressIndicatorActions.START_WORKING("toggleFavorite"));
- const userUuid = getState().auth.user!.uuid;
dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
const isFavorite = checkFavorite(resource.uuid, getState().favorites);
dispatch(snackbarActions.OPEN_SNACKBAR({
export const updateFavorites = (resourceUuids: string[]) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = getState().auth.user!.uuid;
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
services.favoriteService
.checkPresenceInFavorites(userUuid, resourceUuids)
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { PermissionResource } from '~/models/permission';
-import { GroupDetailsPanel } from '~/views/group-details-panel/group-details-panel';
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { UserResource, getUserFullname } from '~/models/user';
import { GroupResource } from '~/models/group';
import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { PermissionLevel, PermissionResource } from '~/models/permission';
+import { PermissionLevel } from '~/models/permission';
import { PermissionService } from '~/services/permission-service/permission-service';
import { FilterBuilder } from '~/services/api/filter-builder';
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(CREATE_GROUP_FORM, { name: 'Group with the same name already exists.' } as FormErrors));
}
}
/**
- * Group membership is determined by whether the group has can_read permission on an object.
+ * Group membership is determined by whether the group has can_read permission on an object.
* If a group G can_read an object A, then we say A is a member of G.
- *
+ *
* [Permission model docs](https://doc.arvados.org/api/permission-model.html)
*/
export const addGroupMember = async ({ user, group, ...args }: AddGroupMemberArgs) => {
import { RootState } from "~/store/store";
import { ServiceRepository } from "~/services/services";
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { getDataExplorer, DataExplorer, getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { getDataExplorer, getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
import { GroupsPanelActions } from '~/store/groups-panel/groups-panel-actions';
import { FilterBuilder } from '~/services/api/filter-builder';
import { updateResources } from '~/store/resources/resources-actions';
import { Dispatch } from "redux";
import { RootState } from "~/store/store";
-import { ServiceRepository } from "~/services/services";
+import { getUserUuid } from "~/common/getuser";
+import { ServiceRepository, createServices, setAuthorizationHeader } from "~/services/services";
import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import { LinkAccountType, AccountToLink, LinkAccountStatus } from "~/models/link-account";
-import { saveApiToken, saveUser } from "~/store/auth/auth-action";
+import { authActions, getConfig } from "~/store/auth/auth-action";
import { unionize, ofType, UnionOf } from '~/common/unionize';
import { UserResource } from "~/models/user";
import { GroupResource } from "~/models/group";
import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
-import { login, logout, setAuthorizationHeader } from "~/store/auth/auth-action";
+import { login, logout } from "~/store/auth/auth-action";
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
export const linkAccountPanelActions = unionize({
LINK_INIT: ofType<{
- targetUser: UserResource | undefined }>(),
+ targetUser: UserResource | undefined
+ }>(),
LINK_LOAD: ofType<{
originatingUser: OriginatingUser | undefined,
targetUser: UserResource | undefined,
targetUserToken: string | undefined,
userToLink: UserResource | undefined,
- userToLinkToken: string | undefined }>(),
+ userToLinkToken: string | undefined
+ }>(),
LINK_INVALID: ofType<{
originatingUser: OriginatingUser | undefined,
targetUser: UserResource | undefined,
userToLink: UserResource | undefined,
- error: LinkAccountPanelError }>(),
+ error: LinkAccountPanelError
+ }>(),
SET_SELECTED_CLUSTER: ofType<{
- selectedCluster: string }>(),
+ selectedCluster: string
+ }>(),
SET_IS_PROCESSING: ofType<{
- isProcessing: boolean}>(),
+ isProcessing: boolean
+ }>(),
HAS_SESSION_DATA: {}
});
return LinkAccountPanelError.NONE;
}
+const newServices = (dispatch: Dispatch<any>, token: string) => {
+ const config = dispatch<any>(getConfig);
+ const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
+ setAuthorizationHeader(svc, token);
+ return svc;
+};
+
export const checkForLinkStatus = () =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const status = services.linkAccountService.getLinkOpStatus();
export const switchUser = (user: UserResource, token: string) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- dispatch(saveUser(user));
- dispatch(saveApiToken(token));
+ dispatch(authActions.INIT_USER({ user, token }));
};
export const linkFailed = () =>
dispatch(checkForLinkStatus());
// Continue loading the link account panel
- dispatch(setBreadcrumbs([{ label: 'Link account'}]));
+ dispatch(setBreadcrumbs([{ label: 'Link account' }]));
const curUser = getState().auth.user;
const curToken = getState().auth.apiToken;
if (curUser && curToken) {
// Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
// issues since a user will always be able to query the api server for their own user data.
- setAuthorizationHeader(services, linkAccountData.token);
- const savedUserResource = await services.userService.get(linkAccountData.userUuid);
- setAuthorizationHeader(services, curToken);
+ const svc = newServices(dispatch, linkAccountData.token);
+ const savedUserResource = await svc.userService.get(linkAccountData.userUuid);
let params: any;
if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT || linkAccountData.type === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
originatingUser: params.originatingUser,
targetUser: params.targetUser,
userToLink: params.userToLink,
- error}));
+ error
+ }));
return;
}
}
export const startLinking = (t: LinkAccountType) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
+ const accountToLink = { type: t, userUuid, token: services.authService.getApiToken() } as AccountToLink;
services.linkAccountService.saveAccountToLink(accountToLink);
const auth = getState().auth;
- const isLocalUser = auth.user!.uuid.substring(0,5) === auth.localCluster;
+ const isLocalUser = auth.user!.uuid.substring(0, 5) === auth.localCluster;
let homeCluster = auth.localCluster;
if (isLocalUser && t === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
homeCluster = getState().linkAccountPanel.selectedCluster!;
}
dispatch(logout());
- dispatch(login(auth.localCluster, homeCluster, auth.remoteHosts));
+ dispatch(login(auth.localCluster, homeCluster, auth.loginCluster, auth.remoteHosts));
};
export const getAccountLinkData = () =>
const linkAccountData = services.linkAccountService.getAccountToLink();
if (linkAccountData) {
services.linkAccountService.removeAccountToLink();
- setAuthorizationHeader(services, linkAccountData.token);
- user = await services.userService.get(linkAccountData.userUuid);
+ const svc = newServices(dispatch, linkAccountData.token);
+ user = await svc.userService.get(linkAccountData.userUuid);
dispatch(switchUser(user, linkAccountData.token));
services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.CANCELLED);
}
try {
// The merge api links the user sending the request into the user
// specified in the request, so change the authorization header accordingly
- setAuthorizationHeader(services, linkState.userToLinkToken);
- await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
+ const svc = newServices(dispatch, linkState.userToLinkToken);
+ await svc.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
services.linkAccountService.removeAccountToLink();
services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.SUCCESS);
location.reload();
}
- catch(e) {
+ catch (e) {
// If the link operation fails, delete the previously made project
try {
- setAuthorizationHeader(services, linkState.targetUserToken);
- await services.projectService.delete(newGroup.uuid);
+ const svc = newServices(dispatch, linkState.targetUserToken);
+ await svc.projectService.delete(newGroup.uuid);
}
finally {
dispatch(linkFailed());
throw e;
}
}
- };
\ No newline at end of file
+ };
import { linkAccountPanelReducer, LinkAccountPanelError, LinkAccountPanelStatus, OriginatingUser } from "~/store/link-account-panel/link-account-panel-reducer";
import { linkAccountPanelActions } from "~/store/link-account-panel/link-account-panel-actions";
-import { UserResource } from "~/models/user";
describe('link-account-panel-reducer', () => {
const initialState = undefined;
export const loadMyAccountPanel = () =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- dispatch(setBreadcrumbs([{ label: 'User profile'}]));
+ dispatch(setBreadcrumbs([{ label: 'User profile' }]));
};
export const saveEditedUser = (resource: any) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
try {
await services.userService.update(resource.uuid, resource);
- services.authService.saveUser(resource);
dispatch(authActions.USER_DETAILS_SUCCESS(resource));
dispatch(initialize(MY_ACCOUNT_FORM, resource));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Profile has been updated.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
- } catch(e) {
+ } catch (e) {
return;
}
};
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch, compose, AnyAction } from 'redux';
-import { push, RouterAction } from "react-router-redux";
+import { push } from "react-router-redux";
import { ResourceKind, extractUuidKind } from '~/models/resource';
-import { getCollectionUrl } from "~/models/collection";
-import { getProjectUrl } from "~/models/project";
import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
-import { Routes, getProcessUrl, getProcessLogUrl, getGroupUrl, getNavUrl } from '~/routes/routes';
+import { Routes, getProcessLogUrl, getGroupUrl, getNavUrl } from '~/routes/routes';
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { GROUPS_PANEL_LABEL } from '~/store/breadcrumbs/breadcrumbs-actions';
export const navigateToProcessLogs = compose(push, getProcessLogUrl);
export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const rootProjectUuid = services.authService.getUuid();
- if (rootProjectUuid) {
- dispatch<any>(navigateTo(rootProjectUuid));
+ const usr = getState().auth.user;
+ if (usr) {
+ dispatch<any>(navigateTo(usr.uuid));
}
};
export const navigateToRunProcess = push(Routes.RUN_PROCESS);
-export const navigateToSearchResults = push(Routes.SEARCH_RESULTS);
+export const navigateToSearchResults = (searchValue: string) => {
+ if (searchValue !== "") {
+ return push({ pathname: Routes.SEARCH_RESULTS, search: '?q=' + encodeURIComponent(searchValue) });
+ } else {
+ return push({ pathname: Routes.SEARCH_RESULTS });
+ }
+};
export const navigateToUserVirtualMachines = push(Routes.VIRTUAL_MACHINES_USER);
import { dialogActions } from '~/store/dialog/dialog-actions';
import { RootState } from '~/store/store';
import { Dispatch } from 'redux';
-import { getProcess } from '~/store/processes/process';
+import { getProcess, Process } from '~/store/processes/process';
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { getWorkflowInputs } from '~/models/workflow';
+import { JSONMount } from '~/models/mount-types';
+import { MOUNT_PATH_CWL_WORKFLOW } from '~/models/process';
export const PROCESS_INPUT_DIALOG_NAME = 'processInputDialog';
const process = getProcess(processUuid)(getState().resources);
if (process) {
const data: any = process;
- if (data && data.containerRequest.mounts.varLibCwlWorkflowJson && data.containerRequest.mounts.varLibCwlWorkflowJson.content.graph.filter((a: any) => a.class === 'Workflow')[0] && data.containerRequest.mounts.varLibCwlWorkflowJson.content.graph.filter((a: any) => a.class === 'Workflow')[0].inputs.length > 0) {
+ const inputs = getInputsFromWFMount(process);
+ if (inputs && inputs.length > 0) {
dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_INPUT_DIALOG_NAME, data }));
} else {
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'There are no inputs in this process!', kind: SnackbarKind.ERROR }));
}
}
- };
\ No newline at end of file
+ };
+
+const getInputsFromWFMount = (process: Process) => {
+ if (!process || !process.containerRequest[MOUNT_PATH_CWL_WORKFLOW] ) { return undefined; }
+ const mnt = process.containerRequest.mounts[MOUNT_PATH_CWL_WORKFLOW] as JSONMount;
+ return getWorkflowInputs(mnt.content);
+};
\ No newline at end of file
return process;
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'A process with the same name already exists in the target project.' } as FormErrors));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
return updatedProcess;
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' } as FormErrors));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
import { navigateToRunProcess } from '~/store/navigation/navigation-action';
import { goToStep, runProcessPanelActions } from '~/store/run-process-panel/run-process-panel-actions';
import { getResource } from '~/store/resources/resources';
-import { getInputValue } from "~/views-components/process-input-dialog/process-input-dialog";
import { initialize } from "redux-form";
import { RUN_PROCESS_BASIC_FORM, RunProcessBasicFormData } from "~/views/run-process-panel/run-process-basic-form";
import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from "~/views/run-process-panel/run-process-advanced-form";
+import { MOUNT_PATH_CWL_WORKFLOW, MOUNT_PATH_CWL_INPUT } from '~/models/process';
+import { getWorkflow, getWorkflowInputs } from "~/models/workflow";
export const loadProcess = (containerRequestUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process> => {
const workflows = getState().runProcessPanel.searchWorkflows;
const workflow = workflows.find(workflow => workflow.uuid === workflowUuid);
if (workflow && process) {
- const newValues = getInputs(process);
- process.mounts.varLibCwlWorkflowJson.content.graph[1].inputs = newValues;
- const stringifiedDefinition = JSON.stringify(process.mounts.varLibCwlWorkflowJson.content);
+ const mainWf = getWorkflow(process.mounts[MOUNT_PATH_CWL_WORKFLOW]);
+ if (mainWf) { mainWf.inputs = getInputs(process); }
+ const stringifiedDefinition = JSON.stringify(process.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
const newWorkflow = { ...workflow, definition: stringifiedDefinition };
const basicInitialData: RunProcessBasicFormData = { name: `Copy of: ${process.name}`, description: process.description };
const advancedInitialData: RunProcessAdvancedFormData = {
output: process.outputName,
- runtime: process.schedulingParameters.maxRunTime,
+ runtime: process.schedulingParameters.max_run_time,
ram: process.runtimeConstraints.ram,
vcpus: process.runtimeConstraints.vcpus,
- keepCacheRam: process.runtimeConstraints.keepCacheRam,
+ keep_cache_ram: process.runtimeConstraints.keep_cache_ram,
api: process.runtimeConstraints.API
};
dispatch<any>(initialize(RUN_PROCESS_ADVANCED_FORM, advancedInitialData));
}
};
-const getInputs = (data: any) =>
- data && data.mounts.varLibCwlWorkflowJson ? data.mounts.varLibCwlWorkflowJson.content.graph[1].inputs.map((it: any) => (
- { type: it.type, id: it.id, label: it.label, default: getInputValue(it.id, data.mounts.varLibCwlCwlInputJson.content), doc: it.doc }
- )) : [];
+const getInputs = (data: any) => {
+ if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) { return []; }
+ const inputs = getWorkflowInputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
+ return inputs ? inputs.map(
+ (it: any) => (
+ {
+ type: it.type,
+ id: it.id,
+ label: it.label,
+ default: data.mounts[MOUNT_PATH_CWL_INPUT].content[it.id],
+ doc: it.doc
+ }
+ )
+ ) : [];
+};
export const openRemoveProcessDialog = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
import { ListResults } from '~/services/common-service/common-service';
import { loadContainers } from '~/store/processes/processes-actions';
import { ResourceKind } from '~/models/resource';
-import { getResource } from "~/store/resources/resources";
-import { CollectionResource } from "~/models/collection";
-import { resourcesDataActions } from "~/store/resources-data/resources-data-actions";
import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
import { serializeResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
import { updatePublicFavorites } from '~/store/public-favorites/public-favorites-actions';
api.dispatch<any>(updateFavorites(resourceUuids));
api.dispatch<any>(updatePublicFavorites(resourceUuids));
api.dispatch(updateResources(response.items));
- api.dispatch<any>(updateResourceData(resourceUuids));
await api.dispatch<any>(loadMissingProcessesInformation(response.items));
api.dispatch(setItems(response));
} catch (e) {
}
};
-export const updateResourceData = (resourceUuids: string[]) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- resourceUuids.map(async uuid => {
- const resource = getResource<CollectionResource>(uuid)(getState().resources);
- if (resource && resource.kind === ResourceKind.COLLECTION) {
- const files = await services.collectionService.files(uuid);
- if (files) {
- dispatch(resourcesDataActions.SET_FILES({ uuid, files }));
- }
- }
- });
- };
-
export const setItems = (listResults: ListResults<GroupContentsResource>) =>
projectPanelActions.SET_ITEMS({
...listResultsToDataExplorerItemsMeta(listResults),
import { Dispatch } from "redux";
import { RootState } from "~/store/store";
+import { getUserUuid } from "~/common/getuser";
import { ServiceRepository } from "~/services/services";
import { mockProjectResource } from "~/models/test-utils";
import { treePickerActions, receiveTreePickerProjectsData } from "~/store/tree-picker/tree-picker-actions";
import { TreePickerId } from '~/models/tree';
export const resetPickerProjectTree = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch<any>(treePickerActions.RESET_TREE_PICKER({pickerId: TreePickerId.PROJECTS}));
- dispatch<any>(treePickerActions.RESET_TREE_PICKER({pickerId: TreePickerId.SHARED_WITH_ME}));
- dispatch<any>(treePickerActions.RESET_TREE_PICKER({pickerId: TreePickerId.FAVORITES}));
+ dispatch<any>(treePickerActions.RESET_TREE_PICKER({ pickerId: TreePickerId.PROJECTS }));
+ dispatch<any>(treePickerActions.RESET_TREE_PICKER({ pickerId: TreePickerId.SHARED_WITH_ME }));
+ dispatch<any>(treePickerActions.RESET_TREE_PICKER({ pickerId: TreePickerId.FAVORITES }));
dispatch<any>(initPickerProjectTree());
};
export const initPickerProjectTree = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const uuid = services.authService.getUuid();
-
+ const uuid = getUserUuid(getState());
+ if (!uuid) { return; }
dispatch<any>(getPickerTreeProjects(uuid));
dispatch<any>(getSharedWithMeProjectsPickerTree(uuid));
dispatch<any>(getFavoritesProjectsPickerTree(uuid));
[mockProjectResource({ uuid, name: kind })],
kind
);
-};
\ No newline at end of file
+};
import { Dispatch } from "redux";
import { reset, startSubmit, stopSubmit, initialize, FormErrors, formValueSelector, change } from 'redux-form';
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { dialogActions } from "~/store/dialog/dialog-actions";
import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
import { ProjectResource } from '~/models/project';
if (properties.breadcrumbs) {
return Boolean(properties.breadcrumbs[0].label !== 'Projects');
} else {
- return ;
+ return;
}
};
const router = getState();
const properties = getState().properties;
if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
- const userUuid = getState().auth.user!.uuid;
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
dispatch(initialize(PROJECT_CREATE_FORM_NAME, { userUuid }));
} else {
dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid }));
return newProject;
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
}
return undefined;
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
properties[data.key] = data.value;
- dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties ));
+ dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties));
};
export const removePropertyFromCreateProjectForm = (key: string) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
delete properties[key];
- dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties ));
- };
\ No newline at end of file
+ dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties));
+ };
import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
import { ServiceRepository } from '~/services/services';
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
export const moveProject = (resource: MoveToFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = getState().auth.user!.uuid;
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
dispatch(startSubmit(PROJECT_MOVE_FORM_NAME));
try {
- const project = await services.projectService.get(resource.uuid);
- const newProject = await services.projectService.update(resource.uuid, { ...project, ownerUuid: resource.ownerUuid });
+ const newProject = await services.projectService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
await dispatch<any>(loadSidePanelTreeProjects(userUuid));
return newProject;
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'A project with the same name already exists in the target project.' } as FormErrors));
} else if (error === CommonResourceServiceError.OWNERSHIP_CYCLE) {
dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'Cannot move a project into itself.' } as FormErrors));
return updatedProject;
} catch (e) {
const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
}
return ;
}
try {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
- const uuidPrefix = api.getState().config.uuidPrefix;
+ const uuidPrefix = api.getState().auth.config.uuidPrefix;
const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
const responseLinks = await this.services.linkService.list({
limit: dataExplorer.rowsPerPage,
snackbarActions.OPEN_SNACKBAR({
message: 'Could not fetch public favorites contents.',
kind: SnackbarKind.ERROR
- });
\ No newline at end of file
+ });
export const togglePublicFavorite = (resource: { uuid: string; name: string }) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
dispatch(progressIndicatorActions.START_WORKING("togglePublicFavorite"));
- const uuidPrefix = getState().config.uuidPrefix;
+ const uuidPrefix = getState().auth.config.uuidPrefix;
const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
dispatch(publicFavoritesActions.TOGGLE_PUBLIC_FAVORITE({ resourceUuid: resource.uuid }));
const isPublicFavorite = checkPublicFavorite(resource.uuid, getState().publicFavorites);
export const updatePublicFavorites = (resourceUuids: string[]) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const uuidPrefix = getState().config.uuidPrefix;
+ const uuidPrefix = getState().auth.config.uuidPrefix;
const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
dispatch(publicFavoritesActions.CHECK_PRESENCE_IN_PUBLIC_FAVORITES(resourceUuids));
services.favoriteService
import { Dispatch } from "redux";
import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { ServiceRepository } from "~/services/services";
import { navigateToRepositories } from "~/store/navigation/navigation-action";
import { unionize, ofType, UnionOf } from "~/common/unionize";
export const openRepositoryCreateDialog = () =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = await services.authService.getUuid();
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
const user = await services.userService.get(userUuid!);
dispatch(reset(REPOSITORY_CREATE_FORM_NAME));
dispatch(dialogActions.OPEN_DIALOG({ id: REPOSITORY_CREATE_FORM_NAME, data: { user } }));
export const createRepository = (repository: RepositoryResource) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = await services.authService.getUuid();
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
const user = await services.userService.get(userUuid!);
dispatch(startSubmit(REPOSITORY_CREATE_FORM_NAME));
try {
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { unionize, ofType, UnionOf } from "~/common/unionize";
-import { CollectionDirectory, CollectionFile } from "~/models/collection-file";
-import { Tree } from "~/models/tree";
-
-export const resourcesDataActions = unionize({
- SET_FILES: ofType<{uuid: string, files: Tree<CollectionFile | CollectionDirectory>}>()
-});
-
-export type ResourcesDataActions = UnionOf<typeof resourcesDataActions>;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ResourcesDataActions, resourcesDataActions } from "~/store/resources-data/resources-data-actions";
-import { getNodeDescendantsIds, TREE_ROOT_ID } from "~/models/tree";
-import { CollectionFileType } from "~/models/collection-file";
-
-export interface ResourceData {
- fileCount: number;
- fileSize: number;
-}
-
-export type ResourcesDataState = {
- [key: string]: ResourceData
-};
-
-export const resourcesDataReducer = (state: ResourcesDataState = {}, action: ResourcesDataActions) =>
- resourcesDataActions.match(action, {
- SET_FILES: ({uuid, files}) => {
- const flattenFiles = getNodeDescendantsIds(TREE_ROOT_ID)(files).map(id => files[id]);
- const [fileSize, fileCount] = flattenFiles.reduce(([size, cnt], f) =>
- f && f.value.type === CollectionFileType.FILE
- ? [size + f.value.size, cnt + 1]
- : [size, cnt]
- , [0, 0]);
- return {
- ...state,
- [uuid]: { fileCount, fileSize }
- };
- },
- default: () => state,
- });
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ResourceData, ResourcesDataState } from "~/store/resources-data/resources-data-reducer";
-
-export const getResourceData = (id: string) =>
- (state: ResourcesDataState): ResourceData | undefined => state[id];
//
// SPDX-License-Identifier: AGPL-3.0
-import { Dispatch } from "redux";
import { dialogActions } from "~/store/dialog/dialog-actions";
export const RICH_TEXT_EDITOR_DIALOG_NAME = 'richTextEditorDialogName';
-export const openRichTextEditorDialog = (title: string, text: string) =>
+export const openRichTextEditorDialog = (title: string, text: string) =>
dialogActions.OPEN_DIALOG({ id: RICH_TEXT_EDITOR_DIALOG_NAME, data: { title, text } });
\ No newline at end of file
import { unionize, ofType, UnionOf } from "~/common/unionize";
import { ServiceRepository } from "~/services/services";
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { WorkflowResource, getWorkflowInputs, parseWorkflowDefinition } from '~/models/workflow';
import { getFormValues, initialize } from 'redux-form';
import { RUN_PROCESS_BASIC_FORM, RunProcessBasicFormData } from '~/views/run-process-panel/run-process-basic-form';
const basicForm = getFormValues(RUN_PROCESS_BASIC_FORM)(state) as RunProcessBasicFormData;
const inputsForm = getFormValues(RUN_PROCESS_INPUTS_FORM)(state) as WorkflowInputsData;
const advancedForm = getFormValues(RUN_PROCESS_ADVANCED_FORM)(state) as RunProcessAdvancedFormData || DEFAULT_ADVANCED_FORM_VALUES;
- const userUuid = getState().auth.user!.uuid;
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
const pathname = getState().runProcessPanel.processPathname;
const { processOwnerUuid, selectedWorkflow } = state.runProcessPanel;
if (selectedWorkflow) {
api: advancedForm[API_FIELD],
},
schedulingParameters: {
- maxRunTime: advancedForm[RUNTIME_FIELD]
+ max_run_time: advancedForm[RUNTIME_FIELD]
},
containerImage: 'arvados/jobs',
cwd: '/var/spool/cwl',
import { ofType, unionize, UnionOf } from "~/common/unionize";
import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
import { Dispatch } from 'redux';
-import { arrayPush, change, initialize } from 'redux-form';
+import { change, initialize, untouch } from 'redux-form';
import { RootState } from '~/store/store';
import { initUserProject, treePickerActions } from '~/store/tree-picker/tree-picker-actions';
import { ServiceRepository } from '~/services/services';
import { FilterBuilder } from "~/services/api/filter-builder";
-import { ResourceKind, isResourceUuid, extractUuidKind, RESOURCE_UUID_REGEX, COLLECTION_PDH_REGEX } from '~/models/resource';
+import { ResourceKind, RESOURCE_UUID_REGEX, COLLECTION_PDH_REGEX } from '~/models/resource';
import { SearchView } from '~/store/search-bar/search-bar-reducer';
import { navigateTo, navigateToSearchResults } from '~/store/navigation/navigation-action';
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { PropertyValue, SearchBarAdvanceFormData } from '~/models/search-bar';
-import { debounce } from 'debounce';
+import { PropertyValue, SearchBarAdvancedFormData } from '~/models/search-bar';
import * as _ from "lodash";
import { getModifiedKeysValues } from "~/common/objects";
import { activateSearchBarProject } from "~/store/search-bar/search-bar-tree-actions";
import { ListResults } from "~/services/common-service/common-service";
import * as parser from './search-query/arv-parser';
import { Keywords } from './search-query/arv-parser';
+import { Vocabulary, getTagKeyLabel, getTagValueLabel } from "~/models/vocabulary";
export const searchBarActions = unionize({
SET_CURRENT_VIEW: ofType<string>(),
CLOSE_SEARCH_VIEW: ofType<{}>(),
SET_SEARCH_RESULTS: ofType<GroupContentsResource[]>(),
SET_SEARCH_VALUE: ofType<string>(),
- SET_SAVED_QUERIES: ofType<SearchBarAdvanceFormData[]>(),
+ SET_SAVED_QUERIES: ofType<SearchBarAdvancedFormData[]>(),
SET_RECENT_QUERIES: ofType<string[]>(),
- UPDATE_SAVED_QUERY: ofType<SearchBarAdvanceFormData[]>(),
+ UPDATE_SAVED_QUERY: ofType<SearchBarAdvancedFormData[]>(),
SET_SELECTED_ITEM: ofType<string>(),
MOVE_UP: ofType<{}>(),
MOVE_DOWN: ofType<{}>(),
export type SearchBarActions = UnionOf<typeof searchBarActions>;
-export const SEARCH_BAR_ADVANCE_FORM_NAME = 'searchBarAdvanceFormName';
+export const SEARCH_BAR_ADVANCED_FORM_NAME = 'searchBarAdvancedFormName';
-export const SEARCH_BAR_ADVANCE_FORM_PICKER_ID = 'searchBarAdvanceFormPickerId';
+export const SEARCH_BAR_ADVANCED_FORM_PICKER_ID = 'searchBarAdvancedFormPickerId';
export const DEFAULT_SEARCH_DEBOUNCE = 1000;
dispatch<any>(searchGroups(searchValue, 5));
if (currentView === SearchView.BASIC) {
dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
- dispatch(navigateToSearchResults);
+ dispatch(navigateToSearchResults(searchValue));
}
}
};
-export const searchAdvanceData = (data: SearchBarAdvanceFormData) =>
- async (dispatch: Dispatch) => {
+export const searchAdvancedData = (data: SearchBarAdvancedFormData) =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
dispatch<any>(saveQuery(data));
+ const searchValue = getState().searchBar.searchValue;
dispatch(searchResultsPanelActions.CLEAR());
dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
- dispatch(navigateToSearchResults);
+ dispatch(navigateToSearchResults(searchValue));
};
-export const setSearchValueFromAdvancedData = (data: SearchBarAdvanceFormData, prevData?: SearchBarAdvanceFormData) =>
+export const setSearchValueFromAdvancedData = (data: SearchBarAdvancedFormData, prevData?: SearchBarAdvancedFormData) =>
(dispatch: Dispatch, getState: () => RootState) => {
const searchValue = getState().searchBar.searchValue;
const value = getQueryFromAdvancedData({
dispatch(searchBarActions.SET_SEARCH_VALUE(value));
};
-export const setAdvancedDataFromSearchValue = (search: string) =>
+export const setAdvancedDataFromSearchValue = (search: string, vocabulary: Vocabulary) =>
async (dispatch: Dispatch) => {
- const data = getAdvancedDataFromQuery(search);
- dispatch<any>(initialize(SEARCH_BAR_ADVANCE_FORM_NAME, data));
+ const data = getAdvancedDataFromQuery(search, vocabulary);
+ dispatch<any>(initialize(SEARCH_BAR_ADVANCED_FORM_NAME, data));
if (data.projectUuid) {
await dispatch<any>(activateSearchBarProject(data.projectUuid));
- dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID, id: data.projectUuid }));
+ dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID, id: data.projectUuid }));
}
};
-const saveQuery = (data: SearchBarAdvanceFormData) =>
+const saveQuery = (data: SearchBarAdvancedFormData) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const savedQueries = services.searchService.getSavedQueries();
if (data.saveQuery && data.queryName) {
return savedSearchQueries || [];
};
-export const editSavedQuery = (data: SearchBarAdvanceFormData) =>
+export const editSavedQuery = (data: SearchBarAdvancedFormData) =>
(dispatch: Dispatch<any>) => {
dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.ADVANCED));
dispatch(searchBarActions.SET_SEARCH_VALUE(getQueryFromAdvancedData(data)));
- dispatch<any>(initialize(SEARCH_BAR_ADVANCE_FORM_NAME, data));
+ dispatch<any>(initialize(SEARCH_BAR_ADVANCED_FORM_NAME, data));
};
export const openSearchView = () =>
export const closeAdvanceView = () =>
(dispatch: Dispatch<any>) => {
dispatch(searchBarActions.SET_SEARCH_VALUE(''));
- dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID }));
+ dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID }));
dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
};
dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
dispatch(searchResultsPanelActions.CLEAR());
- dispatch(navigateToSearchResults);
+ dispatch(navigateToSearchResults(searchValue));
}
};
-const startSearch = () =>
- (dispatch: Dispatch, getState: () => RootState) => {
- const searchValue = getState().searchBar.searchValue;
- dispatch<any>(searchData(searchValue));
- };
-
const searchGroups = (searchValue: string, limit: number) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const currentView = getState().searchBar.currentView;
return value;
};
-export const getQueryFromAdvancedData = (data: SearchBarAdvanceFormData, prevData?: SearchBarAdvanceFormData) => {
+export const getQueryFromAdvancedData = (data: SearchBarAdvancedFormData, prevData?: SearchBarAdvancedFormData) => {
let value = '';
- const flatData = (data: SearchBarAdvanceFormData) => {
+ const flatData = (data: SearchBarAdvancedFormData) => {
const fo = {
searchValue: data.searchValue,
type: data.type,
dateFrom: data.dateFrom,
dateTo: data.dateTo,
};
- (data.properties || []).forEach(p => fo[`prop-"${p.key}"`] = `"${p.value}"`);
+ (data.properties || []).forEach(p => fo[`prop-"${p.keyID || p.key}"`] = `"${p.valueID || p.value}"`);
return fo;
};
['to', 'dateTo']
];
_.union(data.properties, prevData ? prevData.properties : [])
- .forEach(p => keyMap.push([`has:"${p.key}"`, `prop-"${p.key}"`]));
+ .forEach(p => keyMap.push([`has:"${p.keyID || p.key}"`, `prop-"${p.keyID || p.key}"`]));
if (prevData) {
- const fd = flatData(data);
- const pfd = flatData(prevData);
const obj = getModifiedKeysValues(flatData(data), flatData(prevData));
value = buildQueryFromKeyMap({
searchValue: data.searchValue,
...obj
- } as SearchBarAdvanceFormData, keyMap, "reuse");
+ } as SearchBarAdvancedFormData, keyMap, "reuse");
} else {
value = buildQueryFromKeyMap(flatData(data), keyMap, "rebuild");
}
return value;
};
-export const getAdvancedDataFromQuery = (query: string): SearchBarAdvanceFormData => {
+export const getAdvancedDataFromQuery = (query: string, vocabulary?: Vocabulary): SearchBarAdvancedFormData => {
const { tokens, searchString } = parser.parseSearchQuery(query);
const getValue = parser.getValue(tokens);
return {
inTrash: parser.isTrashed(tokens),
dateFrom: getValue(Keywords.FROM) || '',
dateTo: getValue(Keywords.TO) || '',
- properties: parser.getProperties(tokens),
+ properties: vocabulary
+ ? parser.getProperties(tokens).map(
+ p => {
+ return {
+ keyID: p.key,
+ key: getTagKeyLabel(p.key, vocabulary),
+ valueID: p.value,
+ value: getTagValueLabel(p.key, p.value, vocabulary),
+ };
+ })
+ : parser.getProperties(tokens),
saveQuery: false,
queryName: ''
};
return date ? date : '';
};
-export const initAdvanceFormProjectsTree = () =>
+export const initAdvancedFormProjectsTree = () =>
(dispatch: Dispatch) => {
- dispatch<any>(initUserProject(SEARCH_BAR_ADVANCE_FORM_PICKER_ID));
+ dispatch<any>(initUserProject(SEARCH_BAR_ADVANCED_FORM_PICKER_ID));
};
-export const changeAdvanceFormProperty = (property: string, value: PropertyValue[] | string = '') =>
+export const changeAdvancedFormProperty = (propertyField: string, value: PropertyValue[] | string = '') =>
(dispatch: Dispatch) => {
- dispatch(change(SEARCH_BAR_ADVANCE_FORM_NAME, property, value));
+ dispatch(change(SEARCH_BAR_ADVANCED_FORM_NAME, propertyField, value));
};
-export const updateAdvanceFormProperties = (propertyValues: PropertyValue) =>
+export const resetAdvancedFormProperty = (propertyField: string) =>
(dispatch: Dispatch) => {
- dispatch(arrayPush(SEARCH_BAR_ADVANCE_FORM_NAME, 'properties', propertyValues));
+ dispatch(change(SEARCH_BAR_ADVANCED_FORM_NAME, propertyField, null));
+ dispatch(untouch(SEARCH_BAR_ADVANCED_FORM_NAME, propertyField));
};
export const moveUp = () =>
SearchBarActions
} from '~/store/search-bar/search-bar-actions';
import { GroupContentsResource } from '~/services/groups-service/groups-service';
-import { SearchBarAdvanceFormData } from '~/models/search-bar';
+import { SearchBarAdvancedFormData } from '~/models/search-bar';
type SearchResult = GroupContentsResource;
export type SearchBarSelectedItem = {
open: boolean;
searchResults: SearchResult[];
searchValue: string;
- savedQueries: SearchBarAdvanceFormData[];
+ savedQueries: SearchBarAdvancedFormData[];
recentQueries: string[];
selectedItem: SearchBarSelectedItem;
}
const makeSelectedItem = (id: string, query?: string): SearchBarSelectedItem => ({ id, query: query ? query : id });
-const makeQueryList = (recentQueries: string[], savedQueries: SearchBarAdvanceFormData[]) => {
+const makeQueryList = (recentQueries: string[], savedQueries: SearchBarAdvancedFormData[]) => {
const recentIds = recentQueries.map((q, idx) => makeSelectedItem(`RQ-${idx}-${q}`, q));
const savedIds = savedQueries.map((q, idx) => makeSelectedItem(`SQ-${idx}-${q.queryName}`, getQueryFromAdvancedData(q)));
return recentIds.concat(savedIds);
import { getNode, getNodeAncestorsIds, initTreeNode, TreeNodeStatus } from "~/models/tree";
import { Dispatch } from "redux";
import { RootState } from "~/store/store";
+import { getUserUuid } from "~/common/getuser";
import { ServiceRepository } from "~/services/services";
import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
import { FilterBuilder } from "~/services/api/filter-builder";
import { OrderBuilder } from "~/services/api/order-builder";
import { ProjectResource } from "~/models/project";
import { resourcesActions } from "~/store/resources/resources-actions";
-import { SEARCH_BAR_ADVANCE_FORM_PICKER_ID } from "~/store/search-bar/search-bar-actions";
+import { SEARCH_BAR_ADVANCED_FORM_PICKER_ID } from "~/store/search-bar/search-bar-actions";
const getSearchBarTreeNode = (id: string) => (treePicker: TreePicker) => {
- const searchTree = getTreePicker(SEARCH_BAR_ADVANCE_FORM_PICKER_ID)(treePicker);
+ const searchTree = getTreePicker(SEARCH_BAR_ADVANCED_FORM_PICKER_ID)(treePicker);
return searchTree
? getNode(id)(searchTree)
: undefined;
export const loadSearchBarTreeProjects = (projectUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState) => {
- const treePicker = getTreePicker(SEARCH_BAR_ADVANCE_FORM_PICKER_ID)(getState().treePicker);
+ const treePicker = getTreePicker(SEARCH_BAR_ADVANCED_FORM_PICKER_ID)(getState().treePicker);
const node = treePicker ? getNode(projectUuid)(treePicker) : undefined;
if (node || projectUuid === '') {
await dispatch<any>(loadSearchBarProject(projectUuid));
};
export const getSearchBarTreeNodeAncestorsIds = (id: string) => (treePicker: TreePicker) => {
- const searchTree = getTreePicker(SEARCH_BAR_ADVANCE_FORM_PICKER_ID)(treePicker);
+ const searchTree = getTreePicker(SEARCH_BAR_ADVANCED_FORM_PICKER_ID)(treePicker);
return searchTree
? getNodeAncestorsIds(id)(searchTree)
: [];
};
export const activateSearchBarTreeBranch = (id: string) =>
- async (dispatch: Dispatch, _: void, services: ServiceRepository) => {
- const ancestors = await services.ancestorsService.ancestors(id, services.authService.getUuid() || '');
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
+ const ancestors = await services.ancestorsService.ancestors(id, userUuid);
for (const ancestor of ancestors) {
await dispatch<any>(loadSearchBarTreeProjects(ancestor.uuid));
...[],
...ancestors.map(ancestor => ancestor.uuid)
],
- pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID
+ pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID
}));
- dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID }));
+ dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID }));
};
export const expandSearchBarTreeItem = (id: string) =>
async (dispatch: Dispatch, getState: () => RootState) => {
const node = getSearchBarTreeNode(id)(getState().treePicker);
if (node && !node.expanded) {
- dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID }));
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID }));
}
};
}
dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
ids: getSearchBarTreeNodeAncestorsIds(id)(treePicker),
- pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID
+ pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID
}));
dispatch<any>(expandSearchBarTreeItem(id));
};
const loadSearchBarProject = (projectUuid: string) =>
async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
- dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: projectUuid, pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID }));
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: projectUuid, pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID }));
const params = {
filters: new FilterBuilder()
.addEqual('ownerUuid', projectUuid)
const { items } = await services.projectService.list(params);
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
id: projectUuid,
- pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID,
+ pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID,
nodes: items.map(item => initTreeNode({ id: item.uuid, value: item })),
}));
dispatch(resourcesActions.SET_RESOURCES(items));
};
-
import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
import { updateResources } from '~/store/resources/resources-actions';
import { SortDirection } from '~/components/data-table/data-column';
-import { SearchResultsPanelColumnNames } from '~/views/search-results-panel/search-results-panel-view';
import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
import { ListResults } from '~/services/common-service/common-service';
import { DataColumns } from '~/components/data-table/data-table';
import { serializeResourceTypeFilters } from '~/store//resource-type-filters/resource-type-filters';
import { ProjectPanelColumnNames } from '~/views/project-panel/project-panel';
-import * as _ from 'lodash';
import { Resource } from '~/models/resource';
export class SearchResultsMiddlewareService extends DataExplorerMiddlewareService {
return;
}
- try {
- const params = getParams(dataExplorer, searchValue);
+ const params = getParams(dataExplorer, searchValue);
- const responses = await Promise.all(sessions.map(session =>
- this.services.groupsService.contents('', params, session)
- ));
+ const initial = {
+ itemsAvailable: 0,
+ items: [] as GroupContentsResource[],
+ kind: '',
+ offset: 0,
+ limit: 10
+ };
- const initial = {
- itemsAvailable: 0,
- items: [] as GroupContentsResource[],
- kind: '',
- offset: 0,
- limit: 10
- };
-
- const mergedResponse = responses.reduce((merged, current) => ({
- ...merged,
- itemsAvailable: merged.itemsAvailable + current.itemsAvailable,
- items: merged.items.concat(current.items)
- }), initial);
-
- api.dispatch(updateResources(mergedResponse.items));
-
- api.dispatch(criteriaChanged
- ? setItems(mergedResponse)
- : appendItems(mergedResponse));
-
- } catch {
- api.dispatch(couldNotFetchSearchResults());
+ if (criteriaChanged) {
+ api.dispatch(setItems(initial));
}
+
+ sessions.map(session =>
+ this.services.groupsService.contents('', params, session)
+ .then((response) => {
+ api.dispatch(updateResources(response.items));
+ api.dispatch(appendItems(response));
+ }).catch(() => {
+ api.dispatch(couldNotFetchSearchResults(session.clusterId));
+ })
+ );
}
}
items: listResults.items.map(resource => resource.uuid),
});
-const couldNotFetchSearchResults = () =>
+const couldNotFetchSearchResults = (cluster: string) =>
snackbarActions.OPEN_SNACKBAR({
- message: `Could not fetch search results for some sessions.`,
+ message: `Could not fetch search results from ${cluster}.`,
kind: SnackbarKind.ERROR
});
import { ServiceRepository } from '~/services/services';
import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+import { searchBarActions } from '~/store/search-bar/search-bar-actions';
export const SEARCH_RESULTS_PANEL_ID = "searchResultsPanel";
export const searchResultsPanelActions = bindDataExplorerActions(SEARCH_RESULTS_PANEL_ID);
export const loadSearchResultsPanel = () =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(setBreadcrumbs([{ label: 'Search results' }]));
+ const loc = getState().router.location;
+ if (loc !== null) {
+ const search = new URLSearchParams(loc.search);
+ const q = search.get('q');
+ if (q !== null) {
+ dispatch(searchBarActions.SET_SEARCH_VALUE(q));
+ }
+ }
+ dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
+ dispatch(searchResultsPanelActions.CLEAR());
dispatch(searchResultsPanelActions.REQUEST_ITEMS(true));
- };
\ No newline at end of file
+ };
import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
import { extractUuidKind, ResourceKind } from "~/models/resource";
-import { LinkClass } from "~/models/link";
export const openSharingDialog = (resourceUuid: string) =>
(dispatch: Dispatch) => {
};
const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
- async (dispatch: Dispatch, getState: () => RootState, { userService }: ServiceRepository) => {
+ async (dispatch: Dispatch, getState: () => RootState, { userService, groupsService }: ServiceRepository) => {
const filters = new FilterBuilder()
.addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
.getFilters();
const { items: users } = await userService.list({ filters });
+ const { items: groups} = await groupsService.list({ filters });
const getEmail = (tailUuid: string) => {
const user = users.find(({ uuid }) => uuid === tailUuid);
+ const group = groups.find(({ uuid }) => uuid === tailUuid);
return user
? user.email
- : tailUuid;
+ : group
+ ? group.name
+ : tailUuid;
};
const managementPermissions = permissionLinks
const getGroupsFromForm = invitations.invitedPeople.filter((invitation) => extractUuidKind(invitation.uuid) === ResourceKind.GROUP);
const getUsersFromForm = invitations.invitedPeople.filter((invitation) => extractUuidKind(invitation.uuid) === ResourceKind.USER);
- const uuids = getGroupsFromForm.map(group => group.uuid);
-
- const permissions = await permissionService.list({
- filters: new FilterBuilder()
- .addIn('tailUuid', uuids)
- .addEqual('linkClass', LinkClass.PERMISSION)
- .getFilters()
- });
-
- const usersFromGroups = await userService.list({
- filters: new FilterBuilder()
- .addIn('uuid', permissions.items.map(item => item.headUuid))
- .getFilters()
-
- });
const invitationDataUsers = getUsersFromForm
.map(person => ({
name: invitations.permissions
}));
- const invitationsDataGroups = usersFromGroups.items.map(
- person => ({
+ const invitationsDataGroups = getGroupsFromForm.map(
+ group => ({
ownerUuid: user.uuid,
headUuid: dialog.data,
- tailUuid: person.uuid,
+ tailUuid: group.uuid,
name: invitations.permissions
})
);
import { Dispatch } from 'redux';
import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { ServiceRepository } from '~/services/services';
import { FilterBuilder } from '~/services/api/filter-builder';
import { resourcesActions } from '~/store/resources/resources-actions';
export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.some(category => category === id);
export const initSidePanelTree = () =>
- (dispatch: Dispatch, _: () => RootState, { authService }: ServiceRepository) => {
- const rootProjectUuid = authService.getUuid() || '';
+ (dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => {
+ const rootProjectUuid = getUserUuid(getState());
+ if (!rootProjectUuid) { return; }
const nodes = SIDE_PANEL_CATEGORIES.map(id => initTreeNode({ id, value: id }));
const projectsNode = initTreeNode({ id: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS });
const sharedNode = initTreeNode({ id: SidePanelTreeCategory.SHARED_WITH_ME, value: SidePanelTreeCategory.SHARED_WITH_ME });
};
export const activateSidePanelTreeBranch = (id: string) =>
- async (dispatch: Dispatch, _: void, services: ServiceRepository) => {
- const ancestors = await services.ancestorsService.ancestors(id, services.authService.getUuid() || '');
- const isShared = ancestors.every(({ uuid }) => uuid !== services.authService.getUuid());
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ 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));
}
import { History } from "history";
import { authReducer } from "./auth/auth-reducer";
-import { configReducer } from "./config/config-reducer";
+import { authMiddleware } from "./auth/auth-middleware";
import { dataExplorerReducer } from './data-explorer/data-explorer-reducer';
import { detailsPanelReducer } from './details-panel/details-panel-reducer';
import { contextMenuReducer } from './context-menu/context-menu-reducer';
import { treePickerReducer } from './tree-picker/tree-picker-reducer';
import { resourcesReducer } from '~/store/resources/resources-reducer';
import { propertiesReducer } from './properties/properties-reducer';
-import { RootState } from './store';
import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-middleware-service";
import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
import { searchBarReducer } from './search-bar/search-bar-reducer';
import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service';
-import { resourcesDataReducer } from "~/store/resources-data/resources-data-reducer";
import { virtualMachinesReducer } from "~/store/virtual-machines/virtual-machines-reducer";
import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer';
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
+ authMiddleware(services),
projectPanelMiddleware,
favoritePanelMiddleware,
trashPanelMiddleware,
const createRootReducer = (services: ServiceRepository) => combineReducers({
auth: authReducer(services),
- config: configReducer,
collectionPanel: collectionPanelReducer,
collectionPanelFiles: collectionPanelFilesReducer,
contextMenu: contextMenuReducer,
processLogsPanel: processLogsPanelReducer,
properties: propertiesReducer,
resources: resourcesReducer,
- resourcesData: resourcesDataReducer,
router: routerReducer,
snackbar: snackbarReducer,
treePicker: treePickerReducer,
listResultsToDataExplorerItemsMeta
} from "../data-explorer/data-explorer-middleware-service";
import { RootState } from "../store";
+import { getUserUuid } from "~/common/getuser";
import { DataColumns } from "~/components/data-table/data-table";
import { ServiceRepository } from "~/services/services";
import { SortDirection } from "~/components/data-table/data-column";
.addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
}
+ const userUuid = getUserUuid(api.getState());
+ if (!userUuid) { return; }
try {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
- const userUuid = this.services.authService.getUuid()!;
const listResults = await this.services.groupsService
.contents(userUuid, {
...dataExplorerToListParams(dataExplorer),
message: 'Could not fetch trash contents.',
kind: SnackbarKind.ERROR
});
-
import { unionize, ofType, UnionOf } from "~/common/unionize";
import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId, Tree } from '~/models/tree';
+import { createCollectionFilesTree } from "~/models/collection-file";
import { Dispatch } from 'redux';
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { ServiceRepository } from '~/services/services';
import { FilterBuilder } from '~/services/api/filter-builder';
import { pipe, values } from 'lodash/fp';
import { ProjectResource } from '~/models/project';
import { mapTree } from '../../models/tree';
import { LinkResource, LinkClass } from "~/models/link";
+import { mapTreeValues } from "~/models/tree";
+import { sortFilesTree } from "~/services/collection-service/collection-service-files-response";
export const treePickerActions = unionize({
LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
const node = getNode(id)(picker);
if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
- const filesTree = await services.collectionService.files(node.value.portableDataHash);
+ const files = await services.collectionService.files(node.value.portableDataHash);
+ const tree = createCollectionFilesTree(files);
+ const sorted = sortFilesTree(tree);
+ const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
dispatch(
treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
export const initUserProject = (pickerId: string) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const uuid = services.authService.getUuid();
+ const uuid = getUserUuid(getState());
if (uuid) {
dispatch(receiveTreePickerData({
id: '',
};
export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const uuid = services.authService.getUuid();
+ const uuid = getUserUuid(getState());
if (uuid) {
dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles }));
}
export const loadFavoritesProject = (params: LoadFavoritesProjectParams) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const { pickerId, includeCollections = false, includeFiles = false } = params;
- const uuid = services.authService.getUuid();
+ const uuid = getUserUuid(getState());
if (uuid) {
-
const filters = pipe(
(fb: FilterBuilder) => includeCollections
? fb.addIsA('headUuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const { pickerId, includeCollections = false, includeFiles = false } = params;
- const uuidPrefix = getState().config.uuidPrefix;
+ const uuidPrefix = getState().auth.config.uuidPrefix;
const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
if (uuid) {
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
- const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
+
+ const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
const { items } = await services.projectService.list(buildParams(ownerUuid));
dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
export const loadFavoriteTreePickerProjects = (id: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const parentId = services.authService.getUuid() || '';
+ const parentId = getUserUuid(getState()) || '';
if (id === '') {
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
export const loadPublicFavoriteTreePickerProjects = (id: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const parentId = services.authService.getUuid() || '';
+ const parentId = getUserUuid(getState()) || '';
if (id === '') {
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
.addAsc('name')
.getOrder()
};
-};
\ No newline at end of file
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import { createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus, expandNode, deactivateNode, deselectNode, selectNode, selectNodes, deselectNodes } from '~/models/tree';
+import { createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus, expandNode, deactivateNode, selectNodes, deselectNodes } from '~/models/tree';
import { TreePicker } from "./tree-picker";
import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
import { compose } from "redux";
import { Dispatch } from "redux";
import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
import { ServiceRepository } from "~/services/services";
import { dialogActions } from '~/store/dialog/dialog-actions';
import { startSubmit, reset } from "redux-form";
import { UserResource } from "~/models/user";
import { getResource } from '~/store/resources/resources';
import { navigateTo, navigateToUsers, navigateToRootProject } from "~/store/navigation/navigation-action";
-import { saveApiToken } from '~/store/auth/auth-action';
+import { authActions } from '~/store/auth/auth-action';
export const USERS_PANEL_ID = 'usersPanel';
export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const { resources } = getState();
const data = getResource<UserResource>(uuid)(resources);
+ const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
if (data) {
- services.authService.saveUser(data);
+ dispatch<any>(authActions.INIT_USER({ user: data, token: `v2/${client.uuid}/${client.apiToken}` }));
+ location.reload();
+ dispatch<any>(navigateToRootProject);
}
- const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
- dispatch<any>(saveApiToken(`v2/${client.uuid}/${client.apiToken}`));
- location.reload();
- dispatch<any>(navigateToRootProject);
};
export const openUserCreateDialog = () =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = await services.authService.getUuid();
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) { return; }
const user = await services.userService.get(userUuid!);
const virtualMachines = await services.virtualMachineService.list();
dispatch(reset(USER_CREATE_FORM_NAME));
const { resources } = getState();
const data = getResource<UserResource>(uuid)(resources);
const isActive = data!.isActive;
- const newActivity = await services.userService.update(uuid, { ...data, isActive: !isActive });
+ let newActivity;
+ if (isActive) {
+ newActivity = await services.userService.unsetup(uuid);
+ } else {
+ newActivity = await services.userService.update(uuid, { isActive: true });
+ }
dispatch<any>(loadUsersPanel());
return newActivity;
};
const { resources } = getState();
const data = getResource<UserResource>(uuid)(resources);
const isAdmin = data!.isAdmin;
- const newActivity = await services.userService.update(uuid, { ...data, isAdmin: !isAdmin });
+ const newActivity = await services.userService.update(uuid, { isAdmin: !isAdmin });
dispatch<any>(loadUsersPanel());
return newActivity;
};
import { Dispatch } from 'redux';
import { ServiceRepository } from '~/services/services';
import { propertiesActions } from '~/store/properties/properties-actions';
-import { VOCABULARY_PROPERTY_NAME, DEFAULT_VOCABULARY } from './vocabulary-selctors';
+import { VOCABULARY_PROPERTY_NAME, DEFAULT_VOCABULARY } from './vocabulary-selectors';
import { isVocabulary } from '~/models/vocabulary';
export const loadVocabulary = async (dispatch: Dispatch, _: {}, { vocabularyService }: ServiceRepository) => {
export const VOCABULARY_PROPERTY_NAME = 'vocabulary';
export const DEFAULT_VOCABULARY: Vocabulary = {
- strict: false,
+ strict_tags: false,
tags: {},
};
import { Dispatch } from 'redux';
import { RootState } from "~/store/store";
+import { getUserUuid } from "~/common/getuser";
import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { favoritePanelActions, loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action';
loadSidePanelTreeProjects,
SidePanelTreeCategory
} from '~/store/side-panel-tree/side-panel-tree-actions';
-import { loadResource, updateResources } from '~/store/resources/resources-actions';
+import { updateResources } from '~/store/resources/resources-actions';
import { projectPanelColumns } from '~/views/project-panel/project-panel';
import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
import { matchRootRoute } from '~/routes/routes';
export const loadProject = (uuid: string) =>
handleFirstTimeLoad(
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = services.authService.getUuid();
+ const userUuid = getUserUuid(getState());
dispatch(setIsProjectPanelTrashed(false));
if (userUuid) {
if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
export const loadCollection = (uuid: string) =>
handleFirstTimeLoad(
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = services.authService.getUuid();
+ const userUuid = getUserUuid(getState());
if (userUuid) {
const match = await loadGroupContentsResource({ uuid, userUuid, services });
match({
export const updateCollection = (data: collectionUpdateActions.CollectionUpdateFormDialogData) =>
async (dispatch: Dispatch) => {
- const collection = await dispatch<any>(collectionUpdateActions.updateCollection(data));
- if (collection) {
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Collection has been successfully updated.",
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS
- }));
- dispatch<any>(updateResources([collection]));
- dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ try {
+ const collection = await dispatch<any>(collectionUpdateActions.updateCollection(data));
+ if (collection) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully updated.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ }
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.errors.join(''), hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
};
//
// SPDX-License-Identifier: AGPL-3.0
-import { isInteger, isNumber } from 'lodash';
+import { isNumber } from 'lodash';
const ERROR_MESSAGE = 'This field must be a float';
export const ERROR_MESSAGE = 'Maximum string length of this field is: ';
export const DEFAULT_MAX_VALUE = 60;
-interface MaxLengthProps {
- maxLengthValue: number;
- defaultErrorMessage: string;
-}
-
-// TODO types for maxLength
export const maxLength: any = (maxLengthValue = DEFAULT_MAX_VALUE, errorMessage = ERROR_MESSAGE) => {
return (value: string) => {
if (value) {
export const ERROR_MESSAGE = 'This field is required.';
-interface RequiredProps {
- value: string;
-}
-
-// TODO types for require
export const require: any = (value: string) => {
return value && value.length > 0 ? undefined : ERROR_MESSAGE;
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+
+const ERROR_MESSAGE = "Name cannot be '.' or '..' or contain '/' characters";
+
+export const invalidNamingRules = [/\//, /^\.{1,2}$/];
+
+export const validName = (value: string) => {
+ return invalidNamingRules.find(aRule => value.match(aRule) !== null)
+ ? ERROR_MESSAGE
+ : undefined;
+};
import { maxLength } from './max-length';
import { isRsaKey } from './is-rsa-key';
import { isRemoteHost } from "./is-remote-host";
+import { validName } from "./valid-name";
export const TAG_KEY_VALIDATION = [require, maxLength(255)];
export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
-export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
+export const PROJECT_NAME_VALIDATION = [require, validName, maxLength(255)];
-export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
+export const COLLECTION_NAME_VALIDATION = [require, validName, maxLength(255)];
export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
export const COLLECTION_PROJECT_VALIDATION = [require];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RouteProps } from "react-router";
+import * as React from "react";
+import { connect, DispatchProp } from "react-redux";
+import { getUrlParameter } from "~/common/url";
+import { navigateToSiteManager } from "~/store/navigation/navigation-action";
+import { addSession } from "~/store/auth/auth-action-session";
+
+export const AddSession = connect()(
+ class extends React.Component<RouteProps & DispatchProp<any>, {}> {
+ componentDidMount() {
+ const search = this.props.location ? this.props.location.search : "";
+ const apiToken = getUrlParameter(search, 'api_token');
+ const baseURL = getUrlParameter(search, 'baseURL');
+
+ this.props.dispatch(addSession(baseURL, apiToken));
+ this.props.dispatch(navigateToSiteManager);
+ }
+ render() {
+ return <div />;
+ }
+ }
+);
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from "@material-ui/core";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from "@material-ui/core";
import { WithDialogProps } from "~/store/dialog/with-dialog";
import { withDialog } from '~/store/dialog/with-dialog';
import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
import { RouteProps } from "react-router";
import * as React from "react";
import { connect, DispatchProp } from "react-redux";
-import { authActions, getUserDetails, saveApiToken } from "~/store/auth/auth-action";
+import { saveApiToken } from "~/store/auth/auth-action";
import { getUrlParameter } from "~/common/url";
import { AuthService } from "~/services/auth-service/auth-service";
import { navigateToRootProject, navigateToLinkAccount } from "~/store/navigation/navigation-action";
-import { User } from "~/models/user";
import { Config } from "~/common/config";
-import { initSessions } from "~/store/auth/auth-action-session";
import { getAccountLinkData } from "~/store/link-account-panel/link-account-panel-actions";
interface ApiTokenProps {
const search = this.props.location ? this.props.location.search : "";
const apiToken = getUrlParameter(search, 'api_token');
const loadMainApp = this.props.loadMainApp;
- this.props.dispatch(saveApiToken(apiToken));
- this.props.dispatch<any>(getUserDetails()).then((user: User) => {
- this.props.dispatch(initSessions(this.props.authService, this.props.config, user));
- }).finally(() => {
+ this.props.dispatch<any>(saveApiToken(apiToken)).finally(() => {
if (loadMainApp) {
if (this.props.dispatch(getAccountLinkData())) {
this.props.dispatch(navigateToLinkAccount);
},
grid: {
padding: '8px 0 0 0'
- }
+ }
});
interface AttributesComputeNodeDialogDataProps {
};
const renderInfo = (info: NodeInfo, classes: any) => {
- const { lastAction, pingSecret, ec2InstanceId, slurmState } = info;
+ const { last_action, ping_secret, ec2_instance_id, slurm_state } = info;
return (
<Grid container direction="row" spacing={16} className={classnames([classes.root, classes.grid])}>
<Grid item xs={5}>Info - Last action</Grid>
- <Grid item xs={7}>{lastAction || '(none)'}</Grid>
+ <Grid item xs={7}>{last_action || '(none)'}</Grid>
<Grid item xs={5}>Info - Ping secret</Grid>
- <Grid item xs={7}>{pingSecret || '(none)'}</Grid>
+ <Grid item xs={7}>{ping_secret || '(none)'}</Grid>
<Grid item xs={5}>Info - ec2 instance id</Grid>
- <Grid item xs={7}>{ec2InstanceId || '(none)'}</Grid>
+ <Grid item xs={7}>{ec2_instance_id || '(none)'}</Grid>
<Grid item xs={5}>Info - Slurm state</Grid>
- <Grid item xs={7}>{slurmState || '(none)'}</Grid>
+ <Grid item xs={7}>{slurm_state || '(none)'}</Grid>
</Grid>
);
};
const renderProperties = (properties: NodeProperties, classes: any) => {
- const { totalRamMb, totalCpuCores, totalScratchMb, cloudNode } = properties;
+ const { total_ram_mb, total_cpu_cores, total_scratch_mb, cloud_node } = properties;
return (
<Grid container direction="row" spacing={16} className={classnames([classes.root, classes.grid])}>
<Grid item xs={5}>Properties - Total ram mb</Grid>
- <Grid item xs={7}>{totalRamMb || '(none)'}</Grid>
+ <Grid item xs={7}>{total_ram_mb || '(none)'}</Grid>
<Grid item xs={5}>Properties - Total scratch mb</Grid>
- <Grid item xs={7}>{totalScratchMb || '(none)'}</Grid>
+ <Grid item xs={7}>{total_scratch_mb || '(none)'}</Grid>
<Grid item xs={5}>Properties - Total cpu cores</Grid>
- <Grid item xs={7}>{totalCpuCores || '(none)'}</Grid>
+ <Grid item xs={7}>{total_cpu_cores || '(none)'}</Grid>
<Grid item xs={5}>Properties - Cloud node size </Grid>
- <Grid item xs={7}>{cloudNode ? cloudNode.size : '(none)'}</Grid>
+ <Grid item xs={7}>{cloud_node ? cloud_node.size : '(none)'}</Grid>
<Grid item xs={5}>Properties - Cloud node price</Grid>
- <Grid item xs={7}>{cloudNode ? cloudNode.price : '(none)'}</Grid>
+ <Grid item xs={7}>{cloud_node ? cloud_node.price : '(none)'}</Grid>
</Grid>
);
};
\ No newline at end of file
import { ContextMenuActionSet } from "../context-menu-action-set";
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "~/components/icon/icon";
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon } from "~/components/icon/icon";
import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon, AdvancedIcon } from '~/components/icon/icon';
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon } from '~/components/icon/icon';
import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
-import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
import { ToggleFavoriteAction } from "~/views-components/context-menu/actions/favorite-action";
import { toggleFavorite } from "~/store/favorites/favorites-actions";
import {
- RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon,
+ RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon,
AdvancedIcon, RemoveIcon, ReRunProcessIcon, LogIcon, InputIcon, CommandIcon, OutputIcon
} from "~/components/icon/icon";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { WorkflowResource } from '~/models/workflow';
import { ResourceStatus } from '~/views/workflow-panel/workflow-panel-view';
import { getUuidPrefix, openRunProcess } from '~/store/workflow-panel/workflow-panel-actions';
-import { getResourceData } from "~/store/resources-data/resources-data";
import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
import { UserResource } from '~/models/user';
import { toggleIsActive, toggleIsAdmin } from '~/store/users/users-actions';
import { LinkResource } from '~/models/link';
import { navigateTo } from '~/store/navigation/navigation-action';
import { withResourceData } from '~/views-components/data-explorer/with-resources';
+import { CollectionResource } from '~/models/collection';
+import { IllegalNamingWarning } from '~/components/warning/warning';
const renderName = (dispatch: Dispatch, item: { name: string; uuid: string, kind: string }) =>
<Grid container alignItems="center" wrap="nowrap" spacing={16}>
</Grid>
<Grid item>
<Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
+ { item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION
+ ? <IllegalNamingWarning name={item.name} />
+ : null }
{item.name}
</Typography>
</Grid>
export const ResourceCluster = (props: { uuid: string }) => {
const CLUSTER_ID_LENGTH = 5;
- const pos = props.uuid.indexOf('-');
+ const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substr(0, pos) : '';
- const ci = pos >= CLUSTER_ID_LENGTH ? (props.uuid.charCodeAt(0) + props.uuid.charCodeAt(1)) % clusterColors.length : 0;
- return <Typography>
- <span style={{
- backgroundColor: clusterColors[ci][0],
- color: clusterColors[ci][1],
- padding: "2px 7px",
- borderRadius: 3
- }}>{clusterId}</span>
- </Typography>;
+ const ci = pos >= CLUSTER_ID_LENGTH ? (((((
+ (props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1))
+ + props.uuid.charCodeAt(2))
+ * props.uuid.charCodeAt(3))
+ + props.uuid.charCodeAt(4))) % clusterColors.length) : 0;
+ return <span style={{
+ backgroundColor: clusterColors[ci][0],
+ color: clusterColors[ci][1],
+ padding: "2px 7px",
+ borderRadius: 3
+ }}>{clusterId}</span>;
};
export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
export const ResourceFileSize = connect(
(state: RootState, props: { uuid: string }) => {
- const resource = getResourceData(props.uuid)(state.resourcesData);
- return { fileSize: resource ? resource.fileSize : 0 };
+ const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+ return { fileSize: resource ? resource.fileSizeTotal : 0 };
})((props: { fileSize?: number }) => renderFileSize(props.fileSize));
const renderOwner = (owner: string) =>
<DetailsAttribute label='Collection UUID' linkToUuid={this.item.uuid} value={this.item.uuid} />
<DetailsAttribute label='Content address' linkToUuid={this.item.portableDataHash} value={this.item.portableDataHash} />
{/* Missing attrs */}
- <DetailsAttribute label='Number of files' value={this.data && this.data.fileCount} />
- <DetailsAttribute label='Content size' value={formatFileSize(this.data && this.data.fileSize)} />
+ <DetailsAttribute label='Number of files' value={this.item.fileCount} />
+ <DetailsAttribute label='Content size' value={formatFileSize(this.item.fileSizeTotal)} />
</div>;
}
}
import * as React from 'react';
import { DetailsResource } from "~/models/details";
-import { ResourceData } from "~/store/resources-data/resources-data-reducer";
export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
- constructor(protected item: T, protected data?: ResourceData) {}
+ constructor(protected item: T) { }
getTitle(): string {
return this.item.name || 'Projects';
abstract getDetails(): React.ReactElement<any>;
getActivity(): React.ReactElement<any> {
- return <div/>;
+ return <div />;
}
}
import { DetailsData } from "./details-data";
import { DetailsResource } from "~/models/details";
import { getResource } from '~/store/resources/resources';
-import { ResourceData } from "~/store/resources-data/resources-data-reducer";
-import { getResourceData } from "~/store/resources-data/resources-data";
import { toggleDetailsPanel, SLIDE_TIMEOUT } from '~/store/details-panel/details-panel-action';
import { FileDetails } from '~/views-components/details-panel/file-details';
import { getNode } from '~/models/tree';
const EMPTY_RESOURCE: EmptyResource = { kind: undefined, name: 'Projects' };
-const getItem = (res: DetailsResource, resourceData?: ResourceData): DetailsData => {
+const getItem = (res: DetailsResource): DetailsData => {
if ('kind' in res) {
switch (res.kind) {
case ResourceKind.PROJECT:
return new ProjectDetails(res);
case ResourceKind.COLLECTION:
- return new CollectionDetails(res, resourceData);
+ return new CollectionDetails(res);
case ResourceKind.PROCESS:
return new ProcessDetails(res);
default:
}
};
-const mapStateToProps = ({ detailsPanel, resources, resourcesData, collectionPanelFiles }: RootState) => {
+const mapStateToProps = ({ detailsPanel, resources, collectionPanelFiles }: RootState) => {
const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource | undefined;
const file = getNode(detailsPanel.resourceUuid)(collectionPanelFiles);
- const resourceData = getResourceData(detailsPanel.resourceUuid)(resourcesData);
return {
isOpened: detailsPanel.isOpened,
- item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData),
+ item: getItem(resource || (file && file.value) || EMPTY_RESOURCE),
};
};
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { compose } from 'redux';
import { connect } from 'react-redux';
import { openProjectPropertiesDialog } from '~/store/details-panel/details-panel-action';
import { ProjectIcon, RenameIcon } from '~/components/icon/icon';
import { DetailsData } from "./details-data";
import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
import { RichTextEditorLink } from '~/components/rich-text-editor-link/rich-text-editor-link';
-import { withStyles, StyleRulesCallback, Chip, WithStyles } from '@material-ui/core';
+import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
+import { Dispatch } from 'redux';
+import { PropertyChipComponent } from '../resource-properties-form/property-chip';
export class ProjectDetails extends DetailsData<ProjectResource> {
getIcon(className?: string) {
}
});
-
interface ProjectDetailsComponentDataProps {
project: ProjectResource;
}
onClick: () => void;
}
-const mapDispatchToProps = ({ onClick: openProjectPropertiesDialog });
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ onClick: () => dispatch<any>(openProjectPropertiesDialog()),
+});
type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDetailsComponentActionProps & WithStyles<CssRules>;
</div>
</DetailsAttribute>
{
- Object.keys(project.properties).map(k => {
- return <Chip key={k} className={classes.tag} label={`${k}: ${project.properties[k]}`} />;
- })
+ Object.keys(project.properties).map(k =>
+ <PropertyChipComponent key={k}
+ propKey={k} propValue={project.properties[k]}
+ className={classes.tag} />
+ )
}
</div>
));
import { InjectedFormProps, Field } from 'redux-form';
import { WithDialogProps } from '~/store/dialog/with-dialog';
import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
+import { ProjectTreePickerField } from '~/views-components/projects-tree-picker/tree-picker-field';
import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from '~/validators/validators';
import { TextField } from "~/components/text-field/text-field";
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
<Field
name="ownerUuid"
component={ProjectTreePickerField}
- validate={COPY_FILE_VALIDATION}
+ validate={COPY_FILE_VALIDATION}
pickerId={pickerId}/>
</span>);
reduxForm<CollectionCreateFormDialogData>({
form: COLLECTION_CREATE_FORM_NAME,
onSubmit: (data, dispatch) => {
- dispatch(createCollection(data));
+ // Somehow an extra field called 'files' gets added, copy
+ // the data object to get rid of it.
+ dispatch(createCollection({ ownerUuid: data.ownerUuid, name: data.name, description: data.description }));
}
})
)(DialogCollectionCreate);
import { InjectedFormProps, Field } from 'redux-form';
import { WithDialogProps } from '~/store/dialog/with-dialog';
import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
+import { ProjectTreePickerField } from '~/views-components/projects-tree-picker/tree-picker-field';
import { MOVE_TO_VALIDATION } from '~/validators/validators';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { PickerIdProp } from "~/store/tree-picker/picker-id";
import { Field } from "redux-form";
import { TextField } from "~/components/text-field/text-field";
import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
-import { ProjectTreePickerField, CollectionTreePickerField } from "~/views-components/project-tree-picker/project-tree-picker";
+import { ProjectTreePickerField, CollectionTreePickerField } from "~/views-components/projects-tree-picker/tree-picker-field";
import { PickerIdProp } from '~/store/tree-picker/picker-id';
export const CollectionNameField = () =>
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { Field, WrappedFieldProps, FieldArray, formValues } from 'redux-form';
+import { Field, WrappedFieldProps, FieldArray } from 'redux-form';
import { TextField, DateTextField } from "~/components/text-field/text-field";
import { CheckboxField } from '~/components/checkbox-field/checkbox-field';
import { NativeSelectField } from '~/components/select-field/select-field';
import { ResourceKind } from '~/models/resource';
import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker';
-import { SEARCH_BAR_ADVANCE_FORM_PICKER_ID } from '~/store/search-bar/search-bar-actions';
+import { SEARCH_BAR_ADVANCED_FORM_PICKER_ID } from '~/store/search-bar/search-bar-actions';
import { SearchBarAdvancedPropertiesView } from '~/views-components/search-bar/search-bar-advanced-properties-view';
import { TreeItem } from "~/components/tree/tree";
import { ProjectsTreePickerItem } from "~/views-components/projects-tree-picker/generic-projects-tree-picker";
-import { PropertyKeyInput } from '~/views-components/resource-properties-form/property-key-field';
-import { PropertyValueInput, PropertyValueFieldProps } from '~/views-components/resource-properties-form/property-value-field';
-import { VocabularyProp, connectVocabulary } from '~/views-components/resource-properties-form/property-field-common';
-import { compose } from 'redux';
+import { PropertyKeyField, } from '~/views-components/resource-properties-form/property-key-field';
+import { PropertyValueField } from '~/views-components/resource-properties-form/property-value-field';
import { connect } from "react-redux";
import { RootState } from "~/store/store";
const ProjectsPicker = (props: WrappedFieldProps) =>
<div style={{ height: '100px', display: 'flex', flexDirection: 'column', overflow: 'overlay' }}>
<HomeTreePicker
- pickerId={SEARCH_BAR_ADVANCE_FORM_PICKER_ID}
+ pickerId={SEARCH_BAR_ADVANCED_FORM_PICKER_ID}
toggleItemActive={
(_: any, { id }: TreeItem<ProjectsTreePickerItem>) => {
props.input.onChange(id);
name="properties"
component={SearchBarAdvancedPropertiesView} />;
-export const SearchBarKeyField = connectVocabulary(
- ({ vocabulary }: VocabularyProp) =>
- <Field
- name='key'
- component={PropertyKeyInput}
- vocabulary={vocabulary} />);
+export const SearchBarKeyField = () =>
+ <PropertyKeyField skipValidation={true} />;
-export const SearchBarValueField = compose(
- connectVocabulary,
- formValues({ propertyKey: 'key' })
-)(
- (props: PropertyValueFieldProps) =>
- <Field
- name='value'
- component={PropertyValueInput}
- {...props} />);
+export const SearchBarValueField = () =>
+ <PropertyValueField skipValidation={true} />;
export const SearchBarSaveSearchField = () =>
<Field
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { User, getUserFullname } from "~/models/user";
import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
-import { Link } from "react-router-dom";
import { UserPanelIcon } from "~/components/icon/icon";
import { DispatchProp, connect } from 'react-redux';
import { logout } from '~/store/auth/auth-action';
const mapStateToProps = (state: RootState): AccountMenuProps => ({
user: state.auth.user,
currentRoute: state.router.location ? state.router.location.pathname : '',
- workbenchURL: state.config.workbenchUrl,
+ workbenchURL: state.auth.config.workbenchUrl,
apiToken: state.auth.apiToken,
localCluster: state.auth.localCluster
});
import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
import { AdminMenuIcon } from "~/components/icon/icon";
import { DispatchProp, connect } from 'react-redux';
-import { logout } from '~/store/auth/auth-action';
import { RootState } from "~/store/store";
import { openRepositoriesPanel } from "~/store/repositories/repositories-actions";
import * as NavigationAction from '~/store/navigation/navigation-action';
({ dispatch }: DispatchProp<any>) =>
<Button
color="inherit"
- onClick={() => dispatch(login("", "", {}))}>
+ onClick={() => dispatch(login("", "", "", {}))}>
Sign in
</Button>);
buildInfo?: string;
children?: ReactNode;
uuidPrefix: string;
+ siteBanner: string;
}
export type MainAppBarProps = MainAppBarDataProps & WithStyles<CssRules>;
<Grid container item xs={3} direction="column" justify="center">
<Typography variant='h6' color="inherit" noWrap>
<Link to={Routes.ROOT} className={props.classes.link}>
- arvados workbench ({props.uuidPrefix})
+ <span dangerouslySetInnerHTML={{ __html: props.siteBanner }} /> ({props.uuidPrefix})
</Link>
</Typography>
<Typography variant="caption" color="inherit">{props.buildInfo}</Typography>
import { withDialog } from "~/store/dialog/with-dialog";
import { PROCESS_INPUT_DIALOG_NAME } from '~/store/processes/process-input-actions';
import { RunProcessInputsForm } from "~/views/run-process-panel/run-process-inputs-form";
+import { MOUNT_PATH_CWL_WORKFLOW, MOUNT_PATH_CWL_INPUT } from "~/models/process";
+import { getWorkflowInputs } from "~/models/workflow";
export const ProcessInputDialog = withDialog(PROCESS_INPUT_DIALOG_NAME)(
(props: WithDialogProps<any>) =>
</Dialog>
);
-const getInputs = (data: any) =>
- data && data.mounts.varLibCwlWorkflowJson ? data.mounts.varLibCwlWorkflowJson.content.graph.filter((a: any) => a.class === 'Workflow')[0].inputs.map((it: any) => (
- { type: it.type, id: it.id, label: it.label, value: getInputValue(it.id, data.mounts.varLibCwlCwlInputJson.content), disabled: true }
- )) : [];
-
-const snakeToCamel = (s: string) => {
- const a = s.split('/');
- return a[1].replace(/(\_\w)/g, (m: string) => m[1].toUpperCase());
+const getInputs = (data: any) => {
+ if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) { return []; }
+ const inputs = getWorkflowInputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
+ return inputs ? inputs.map(
+ (it: any) => (
+ {
+ type: it.type,
+ id: it.id,
+ label: it.label,
+ value: data.mounts[MOUNT_PATH_CWL_INPUT].content[it.id],
+ disabled: true
+ }
+ )
+ ) : [];
};
-
-export const getInputValue = (id: string, data: any) => {
- const a = snakeToCamel(id);
- return data[a];
-};
\ No newline at end of file
import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
import { ProjectResource } from '~/models/project';
import { PROJECT_PROPERTIES_DIALOG_NAME, deleteProjectProperty } from '~/store/details-panel/details-panel-action';
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Chip, withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
import { ProjectPropertiesForm } from '~/views-components/project-properties-dialog/project-properties-form';
import { getResource } from '~/store/resources/resources';
+import { PropertyChipComponent } from "../resource-properties-form/property-chip";
type CssRules = 'tag';
handleDelete: (key: string) => void;
}
-const mapStateToProps = ({ detailsPanel, resources }: RootState): ProjectPropertiesDialogDataProps => {
- const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource;
- return { project };
-};
+const mapStateToProps = ({ detailsPanel, resources, properties }: RootState): ProjectPropertiesDialogDataProps => ({
+ project: getResource(detailsPanel.resourceUuid)(resources) as ProjectResource,
+});
const mapDispatchToProps = (dispatch: Dispatch): ProjectPropertiesDialogActionProps => ({
- handleDelete: (key: string) => dispatch<any>(deleteProjectProperty(key))
+ handleDelete: (key: string) => dispatch<any>(deleteProjectProperty(key)),
});
type ProjectPropertiesDialogProps = ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles<CssRules>;
<DialogTitle>Properties</DialogTitle>
<DialogContent>
<ProjectPropertiesForm />
- {project && project.properties &&
- Object.keys(project.properties).map(k => {
- return <Chip key={k} className={classes.tag}
+ {project && project.properties &&
+ Object.keys(project.properties).map(k =>
+ <PropertyChipComponent
onDelete={() => handleDelete(k)}
- label={`${k}: ${project.properties[k]}`} />;
- })
+ key={k} className={classes.tag}
+ propKey={k} propValue={project.properties[k]} />)
}
</DialogContent>
<DialogActions>
</Button>
</DialogActions>
</Dialog>
-)));
\ No newline at end of file
+ )
+));
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from "react";
-import { Dispatch } from "redux";
-import { connect } from "react-redux";
-import { Typography } from "@material-ui/core";
-import { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
-import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
-import { ProjectResource } from "~/models/project";
-import { treePickerActions, loadProjectTreePickerProjects, loadFavoriteTreePickerProjects, loadPublicFavoriteTreePickerProjects } from "~/store/tree-picker/tree-picker-actions";
-import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
-import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, PublicFavoriteIcon } from '~/components/icon/icon';
-import { RootState } from "~/store/store";
-import { ServiceRepository } from "~/services/services";
-import { WrappedFieldProps } from 'redux-form';
-import { TreePickerId } from '~/models/tree';
-import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
-import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
-import { PickerIdProp } from '~/store/tree-picker/picker-id';
-
-type ProjectTreePickerProps = Pick<TreePickerProps<ProjectResource>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
-
-const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): ProjectTreePickerProps => ({
- onContextMenu: () => { return; },
- toggleItemActive: (_, { id }, pickerId) => {
- getNotSelectedTreePickerKind(pickerId)
- .forEach(pickerId => dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: '', pickerId })));
- dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId }));
-
- props.onChange(id);
- },
- toggleItemOpen: (_, { id, status }, pickerId) => {
- dispatch<any>(toggleItemOpen(id, status, pickerId));
- },
- toggleItemSelection: (_, { id }, pickerId) => {
- dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id, pickerId }));
- },
-});
-
-const toggleItemOpen = (id: string, status: TreeItemStatus, pickerId: string) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- if (status === TreeItemStatus.INITIAL) {
- if (pickerId === TreePickerId.PROJECTS) {
- dispatch<any>(loadProjectTreePickerProjects(id));
- } else if (pickerId === TreePickerId.FAVORITES) {
- dispatch<any>(loadFavoriteTreePickerProjects(id === services.authService.getUuid() ? '' : id));
- } else if (pickerId === TreePickerId.PUBLIC_FAVORITES) {
- dispatch<any>(loadPublicFavoriteTreePickerProjects(id === services.authService.getUuid() ? '' : id));
- // TODO: load sharedWithMe
- }
- } else {
- dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
- }
- };
-
-const getNotSelectedTreePickerKind = (pickerId: string) => {
- return [TreePickerId.PROJECTS, TreePickerId.FAVORITES, TreePickerId.SHARED_WITH_ME].filter(nodeId => nodeId !== pickerId);
-};
-
-export const ProjectTreePicker = connect(undefined, mapDispatchToProps)((props: ProjectTreePickerProps) =>
- <div style={{ display: 'flex', flexDirection: 'column' }}>
- <Typography variant='caption' style={{ flexShrink: 0 }}>
- Select a project
- </Typography>
- <div style={{ flexGrow: 1, overflow: 'auto' }}>
- <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.PROJECTS} />
- <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.SHARED_WITH_ME} />
- <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.FAVORITES} />
- <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.PUBLIC_FAVORITES} />
- </div>
- </div>);
-
-const getProjectPickerIcon = (item: TreeItem<ProjectResource>) => {
- switch (item.data.name) {
- case TreePickerId.FAVORITES:
- return FavoriteIcon;
- case TreePickerId.PROJECTS:
- return ProjectsIcon;
- case TreePickerId.SHARED_WITH_ME:
- return ShareMeIcon;
- case TreePickerId.PUBLIC_FAVORITES:
- return PublicFavoriteIcon;
- default:
- return ProjectIcon;
- }
-};
-
-const renderTreeItem = (item: TreeItem<ProjectResource>) =>
- <ListItemTextIcon
- icon={getProjectPickerIcon(item)}
- name={typeof item.data === 'string' ? item.data : item.data.name}
- isActive={item.active}
- hasMargin={true} />;
-
-export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
- <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
- <ProjectsTreePicker
- pickerId={props.pickerId}
- toggleItemActive={handleChange(props)} />
- {props.meta.dirty && props.meta.error &&
- <Typography variant='caption' color='error'>
- {props.meta.error}
- </Typography>}
- </div>;
-
-const handleChange = (props: WrappedFieldProps) =>
- (_: any, { id }: TreeItem<ProjectsTreePickerItem>) =>
- props.input.onChange(id);
-
-export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
- <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
- <ProjectsTreePicker
- pickerId={props.pickerId}
- toggleItemActive={handleChange(props)}
- includeCollections />
- {props.meta.dirty && props.meta.error &&
- <Typography variant='caption' color='error'>
- {props.meta.error}
- </Typography>}
- </div>;
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import * as Enzyme from 'enzyme';
-import { mount } from 'enzyme';
-import * as Adapter from 'enzyme-adapter-react-16';
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import { Collapse } from '@material-ui/core';
-import CircularProgress from '@material-ui/core/CircularProgress';
-
-import { ProjectTree } from './project-tree';
-import { TreeItem, TreeItemStatus } from '../../components/tree/tree';
-import { ProjectResource } from '../../models/project';
-import { mockProjectResource } from '../../models/test-utils';
-
-Enzyme.configure({ adapter: new Adapter() });
-
-describe("ProjectTree component", () => {
-
- it("should render ListItemIcon", () => {
- const project: TreeItem<ProjectResource> = {
- data: mockProjectResource(),
- id: "3",
- open: true,
- active: true,
- status: TreeItemStatus.PENDING
- };
- const wrapper = mount(<ProjectTree
- projects={[project]}
- toggleOpen={jest.fn()}
- toggleActive={jest.fn()}
- onContextMenu={jest.fn()} />);
-
- expect(wrapper.find(ListItemIcon)).toHaveLength(2);
- });
-
- it("should render Collapse", () => {
- const project: Array<TreeItem<ProjectResource>> = [
- {
- data: mockProjectResource(),
- id: "3",
- open: true,
- active: true,
- status: TreeItemStatus.LOADED,
- items: [
- {
- data: mockProjectResource(),
- id: "3",
- open: true,
- active: true,
- status: TreeItemStatus.PENDING
- }
- ]
- }
- ];
- const wrapper = mount(<ProjectTree
- projects={project}
- toggleOpen={jest.fn()}
- toggleActive={jest.fn()}
- onContextMenu={jest.fn()} />);
-
- expect(wrapper.find(Collapse)).toHaveLength(1);
- });
-
- it("should render CircularProgress", () => {
- const project: TreeItem<ProjectResource> = {
- data: mockProjectResource(),
- id: "3",
- open: false,
- active: true,
- status: TreeItemStatus.PENDING
- };
- const wrapper = mount(<ProjectTree
- projects={[project]}
- toggleOpen={jest.fn()}
- toggleActive={jest.fn()}
- onContextMenu={jest.fn()} />);
-
- expect(wrapper.find(CircularProgress)).toHaveLength(1);
- });
-});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { ReactElement } from 'react';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { Tree, TreeItem, TreeItemStatus } from '~/components/tree/tree';
-import { ProjectResource } from '~/models/project';
-import { ProjectIcon } from '~/components/icon/icon';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { ListItemTextIcon } from '~/components/list-item-text-icon/list-item-text-icon';
-
-type CssRules = 'root';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- root: {
- marginLeft: `${theme.spacing.unit * 1.5}px`,
- }
-});
-
-export interface ProjectTreeProps<T> {
- projects: Array<TreeItem<ProjectResource>>;
- toggleOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
- toggleActive: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<ProjectResource>) => void;
-}
-
-export const ProjectTree = withStyles(styles)(
- class ProjectTreeGeneric<T> extends React.Component<ProjectTreeProps<T> & WithStyles<CssRules>> {
- render(): ReactElement<any> {
- const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props;
- return (
- <div className={classes.root}>
- <Tree items={projects}
- onContextMenu={onContextMenu}
- toggleItemOpen={toggleOpen}
- toggleItemActive={toggleActive}
- render={
- (project: TreeItem<ProjectResource>) =>
- <ListItemTextIcon
- icon={ProjectIcon}
- name={project.data.name}
- isActive={project.active}
- hasMargin={true} />
- } />
- </div>
- );
- }
- }
-);
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { values, memoize, pipe, pick } from 'lodash/fp';
+import { values, memoize, pipe } from 'lodash/fp';
import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker';
import { SharedTreePicker } from '~/views-components/projects-tree-picker/shared-tree-picker';
import { FavoritesTreePicker } from '~/views-components/projects-tree-picker/favorites-tree-picker';
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Typography } from "@material-ui/core";
+import { TreeItem } from "~/components/tree/tree";
+import { WrappedFieldProps } from 'redux-form';
+import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
+import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
+import { PickerIdProp } from '~/store/tree-picker/picker-id';
+
+export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
+ <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
+ <ProjectsTreePicker
+ pickerId={props.pickerId}
+ toggleItemActive={handleChange(props)} />
+ {props.meta.dirty && props.meta.error &&
+ <Typography variant='caption' color='error'>
+ {props.meta.error}
+ </Typography>}
+ </div>;
+
+const handleChange = (props: WrappedFieldProps) =>
+ (_: any, { id }: TreeItem<ProjectsTreePickerItem>) =>
+ props.input.onChange(id);
+
+export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
+ <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
+ <ProjectsTreePicker
+ pickerId={props.pickerId}
+ toggleItemActive={handleChange(props)}
+ includeCollections />
+ {props.meta.dirty && props.meta.error &&
+ <Typography variant='caption' color='error'>
+ {props.meta.error}
+ </Typography>}
+ </div>;
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Chip } from '@material-ui/core';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
+import { getVocabulary } from '~/store/vocabulary/vocabulary-selectors';
+import { Dispatch } from 'redux';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { getTagValueLabel, getTagKeyLabel, Vocabulary } from '~/models/vocabulary';
+
+interface PropertyChipComponentDataProps {
+ propKey: string;
+ propValue: string;
+ className: string;
+ vocabulary: Vocabulary;
+}
+
+interface PropertyChipComponentActionProps {
+ onDelete?: () => void;
+ onCopy: (message: string) => void;
+}
+
+type PropertyChipComponentProps = PropertyChipComponentActionProps & PropertyChipComponentDataProps;
+
+const mapStateToProps = ({ properties }: RootState) => ({
+ vocabulary: getVocabulary(properties),
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ onCopy: (message: string) => dispatch(snackbarActions.OPEN_SNACKBAR({
+ message,
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }))
+});
+
+// Renders a Chip with copyable-on-click tag:value data based on the vocabulary
+export const PropertyChipComponent = connect(mapStateToProps, mapDispatchToProps)(
+ ({ propKey, propValue, vocabulary, className, onCopy, onDelete }: PropertyChipComponentProps) => {
+ const label = `${getTagKeyLabel(propKey, vocabulary)}: ${getTagValueLabel(propKey, propValue, vocabulary)}`;
+ return (
+ <CopyToClipboard key={propKey} text={label} onCopy={() => onCopy("Copied to clipboard")}>
+ <Chip onDelete={onDelete} key={propKey}
+ className={className} label={label} />
+ </CopyToClipboard>
+ );
+ }
+);
// SPDX-License-Identifier: AGPL-3.0
import { connect } from 'react-redux';
-import { WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form';
-import { identity } from 'lodash';
-import { Vocabulary } from '~/models/vocabulary';
+import { change, WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form';
+import { Vocabulary, PropFieldSuggestion } from '~/models/vocabulary';
import { RootState } from '~/store/store';
-import { getVocabulary } from '~/store/vocabulary/vocabulary-selctors';
+import { getVocabulary } from '~/store/vocabulary/vocabulary-selectors';
export interface VocabularyProp {
vocabulary: Vocabulary;
}
-export const mapStateToProps = (state: RootState): VocabularyProp => ({
+export interface ValidationProp {
+ skipValidation?: boolean;
+}
+
+export const mapStateToProps = (state: RootState, ownProps: ValidationProp): VocabularyProp & ValidationProp => ({
+ skipValidation: ownProps.skipValidation,
vocabulary: getVocabulary(state.properties),
});
? meta.error
: '';
-export const handleBlur = ({ onBlur, value }: WrappedFieldInputProps) =>
- () =>
+export const buildProps = ({ input, meta }: WrappedFieldProps) => {
+ return {
+ value: input.value,
+ onChange: input.onChange,
+ items: ITEMS_PLACEHOLDER,
+ renderSuggestion: (item: PropFieldSuggestion) => item.label,
+ error: hasError(meta),
+ helperText: getErrorMsg(meta),
+ };
+};
+
+// Attempts to match a manually typed value label with a value ID, when the user
+// doesn't select the value from the suggestions list.
+export const handleBlur = (
+ fieldName: string,
+ formName: string,
+ { dispatch }: WrappedFieldMetaProps,
+ { onBlur, value }: WrappedFieldInputProps,
+ fieldValue: string) =>
+ () => {
+ dispatch(change(formName, fieldName, fieldValue));
onBlur(value);
+ };
-export const buildProps = ({ input, meta }: WrappedFieldProps) => ({
- value: input.value,
- onChange: input.onChange,
- onBlur: handleBlur(input),
- items: ITEMS_PLACEHOLDER,
- onSelect: input.onChange,
- renderSuggestion: identity,
- error: hasError(meta),
- helperText: getErrorMsg(meta),
-});
+// When selecting a property value, save its ID for later usage.
+export const handleSelect = (
+ fieldName: string,
+ formName: string,
+ { onChange }: WrappedFieldInputProps,
+ { dispatch }: WrappedFieldMetaProps) =>
+ (item: PropFieldSuggestion) => {
+ if (item) {
+ onChange(item.label);
+ dispatch(change(formName, fieldName, item.id));
+ }
+ };
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { WrappedFieldProps, Field } from 'redux-form';
+import { WrappedFieldProps, Field, FormName } from 'redux-form';
import { memoize } from 'lodash';
import { Autocomplete } from '~/components/autocomplete/autocomplete';
-import { Vocabulary } from '~/models/vocabulary';
-import { connectVocabulary, VocabularyProp, buildProps } from '~/views-components/resource-properties-form/property-field-common';
+import { Vocabulary, getTags, getTagKeyID } from '~/models/vocabulary';
+import { handleSelect, handleBlur, connectVocabulary, VocabularyProp, ValidationProp, buildProps } from '~/views-components/resource-properties-form/property-field-common';
import { TAG_KEY_VALIDATION } from '~/validators/validators';
+import { escapeRegExp } from '~/common/regexp.ts';
export const PROPERTY_KEY_FIELD_NAME = 'key';
+export const PROPERTY_KEY_FIELD_ID = 'keyID';
export const PropertyKeyField = connectVocabulary(
- ({ vocabulary }: VocabularyProp) =>
+ ({ vocabulary, skipValidation }: VocabularyProp & ValidationProp) =>
<Field
name={PROPERTY_KEY_FIELD_NAME}
component={PropertyKeyInput}
vocabulary={vocabulary}
- validate={getValidation(vocabulary)} />);
-
-export const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) =>
- <Autocomplete
- label='Key'
- suggestions={getSuggestions(props.input.value, vocabulary)}
- {...buildProps(props)}
- />;
+ validate={skipValidation ? undefined : getValidation(vocabulary)} />
+);
+
+const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) =>
+ <FormName children={data => (
+ <Autocomplete
+ label='Key'
+ suggestions={getSuggestions(props.input.value, vocabulary)}
+ onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
+ onBlur={handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, getTagKeyID(props.input.value, vocabulary))}
+ {...buildProps(props)}
+ />
+ )} />;
const getValidation = memoize(
(vocabulary: Vocabulary) =>
- vocabulary.strict
+ vocabulary.strict_tags
? [...TAG_KEY_VALIDATION, matchTags(vocabulary)]
: TAG_KEY_VALIDATION);
const matchTags = (vocabulary: Vocabulary) =>
(value: string) =>
- getTagsList(vocabulary).find(tag => tag.includes(value))
+ getTags(vocabulary).find(tag => tag.label === value)
? undefined
: 'Incorrect key';
-const getSuggestions = (value: string, vocabulary: Vocabulary) =>
- getTagsList(vocabulary).filter(tag => tag.includes(value) && tag !== value);
-
-const getTagsList = ({ tags }: Vocabulary) =>
- Object.keys(tags);
+const getSuggestions = (value: string, vocabulary: Vocabulary) => {
+ const re = new RegExp(escapeRegExp(value), "i");
+ return getTags(vocabulary).filter(tag => re.test(tag.label) && tag.label !== value);
+};
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { WrappedFieldProps, Field, formValues } from 'redux-form';
+import { WrappedFieldProps, Field, formValues, FormName } from 'redux-form';
import { compose } from 'redux';
import { Autocomplete } from '~/components/autocomplete/autocomplete';
-import { Vocabulary } from '~/models/vocabulary';
-import { PROPERTY_KEY_FIELD_NAME } from '~/views-components/resource-properties-form/property-key-field';
-import { VocabularyProp, connectVocabulary, buildProps } from '~/views-components/resource-properties-form/property-field-common';
+import { Vocabulary, isStrictTag, getTagValues, getTagValueID } from '~/models/vocabulary';
+import { PROPERTY_KEY_FIELD_ID } from '~/views-components/resource-properties-form/property-key-field';
+import { handleSelect, handleBlur, VocabularyProp, ValidationProp, connectVocabulary, buildProps } from '~/views-components/resource-properties-form/property-field-common';
import { TAG_VALUE_VALIDATION } from '~/validators/validators';
+import { escapeRegExp } from '~/common/regexp.ts';
interface PropertyKeyProp {
propertyKey: string;
}
-export type PropertyValueFieldProps = VocabularyProp & PropertyKeyProp;
+type PropertyValueFieldProps = VocabularyProp & PropertyKeyProp & ValidationProp;
export const PROPERTY_VALUE_FIELD_NAME = 'value';
+export const PROPERTY_VALUE_FIELD_ID = 'valueID';
-export const PropertyValueField = compose(
+const connectVocabularyAndPropertyKey = compose(
connectVocabulary,
- formValues({ propertyKey: PROPERTY_KEY_FIELD_NAME })
-)(
- (props: PropertyValueFieldProps) =>
+ formValues({ propertyKey: PROPERTY_KEY_FIELD_ID }),
+);
+
+export const PropertyValueField = connectVocabularyAndPropertyKey(
+ ({ skipValidation, ...props }: PropertyValueFieldProps) =>
<Field
name={PROPERTY_VALUE_FIELD_NAME}
component={PropertyValueInput}
- validate={getValidation(props)}
- {...props} />);
-
-export const PropertyValueInput = ({ vocabulary, propertyKey, ...props }: WrappedFieldProps & PropertyValueFieldProps) =>
- <Autocomplete
- label='Value'
- suggestions={getSuggestions(props.input.value, propertyKey, vocabulary)}
- {...buildProps(props)}
- />;
+ validate={skipValidation ? undefined : getValidation(props)}
+ {...props} />
+);
+
+const PropertyValueInput = ({ vocabulary, propertyKey, ...props }: WrappedFieldProps & PropertyValueFieldProps) =>
+ <FormName children={data => (
+ <Autocomplete
+ label='Value'
+ suggestions={getSuggestions(props.input.value, propertyKey, vocabulary)}
+ onSelect={handleSelect(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
+ onBlur={handleBlur(PROPERTY_VALUE_FIELD_ID, data.form, props.meta, props.input, getTagValueID(propertyKey, props.input.value, vocabulary))}
+ {...buildProps(props)}
+ />
+ )} />;
const getValidation = (props: PropertyValueFieldProps) =>
isStrictTag(props.propertyKey, props.vocabulary)
const matchTagValues = ({ vocabulary, propertyKey }: PropertyValueFieldProps) =>
(value: string) =>
- getTagValues(propertyKey, vocabulary).find(v => v.includes(value))
+ getTagValues(propertyKey, vocabulary).find(v => v.label === value)
? undefined
: 'Incorrect value';
-const getSuggestions = (value: string, tagName: string, vocabulary: Vocabulary) =>
- getTagValues(tagName, vocabulary).filter(v => v.includes(value) && v !== value);
-
-const isStrictTag = (tagName: string, vocabulary: Vocabulary) => {
- const tag = vocabulary.tags[tagName];
- return tag ? tag.strict : false;
-};
-
-const getTagValues = (tagName: string, vocabulary: Vocabulary) => {
- const tag = vocabulary.tags[tagName];
- return tag && tag.values ? tag.values : [];
+const getSuggestions = (value: string, tagName: string, vocabulary: Vocabulary) => {
+ const re = new RegExp(escapeRegExp(value), "i");
+ return getTagValues(tagName, vocabulary).filter(v => re.test(v.label) && v.label !== value);
};
import * as React from 'react';
import { InjectedFormProps } from 'redux-form';
import { Grid, withStyles, WithStyles } from '@material-ui/core';
-import { PropertyKeyField, PROPERTY_KEY_FIELD_NAME } from './property-key-field';
-import { PropertyValueField, PROPERTY_VALUE_FIELD_NAME } from './property-value-field';
+import { PropertyKeyField, PROPERTY_KEY_FIELD_NAME, PROPERTY_KEY_FIELD_ID } from './property-key-field';
+import { PropertyValueField, PROPERTY_VALUE_FIELD_NAME, PROPERTY_VALUE_FIELD_ID } from './property-value-field';
import { ProgressButton } from '~/components/progress-button/progress-button';
import { GridClassKey } from '@material-ui/core/Grid';
export interface ResourcePropertiesFormData {
[PROPERTY_KEY_FIELD_NAME]: string;
+ [PROPERTY_KEY_FIELD_ID]: string;
[PROPERTY_VALUE_FIELD_NAME]: string;
+ [PROPERTY_VALUE_FIELD_ID]: string;
}
export type ResourcePropertiesFormProps = InjectedFormProps<ResourcePropertiesFormData> & WithStyles<GridClassKey>;
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Dispatch } from 'redux';
+import { Dispatch, compose } from 'redux';
import { connect } from 'react-redux';
import { InjectedFormProps, formValueSelector } from 'redux-form';
import { Grid, withStyles, StyleRulesCallback, WithStyles, Button } from '@material-ui/core';
import { RootState } from '~/store/store';
import {
- SEARCH_BAR_ADVANCE_FORM_NAME,
- changeAdvanceFormProperty,
- updateAdvanceFormProperties
+ SEARCH_BAR_ADVANCED_FORM_NAME,
+ changeAdvancedFormProperty,
+ resetAdvancedFormProperty
} from '~/store/search-bar/search-bar-actions';
import { PropertyValue } from '~/models/search-bar';
import { ArvadosTheme } from '~/common/custom-theme';
import { SearchBarKeyField, SearchBarValueField } from '~/views-components/form-fields/search-bar-form-fields';
import { Chips } from '~/components/chips/chips';
import { formatPropertyValue } from "~/common/formatters";
+import { Vocabulary } from '~/models/vocabulary';
+import { connectVocabulary } from '../resource-properties-form/property-field-common';
type CssRules = 'label' | 'button';
pristine: boolean;
propertyValues: PropertyValue;
fields: PropertyValue[];
+ vocabulary: Vocabulary;
}
interface SearchBarAdvancedPropertiesViewActionProps {
setProps: () => void;
- addProp: (propertyValues: PropertyValue) => void;
+ setProp: (propertyValues: PropertyValue, properties: PropertyValue[]) => void;
getAllFields: (propertyValues: PropertyValue[]) => PropertyValue[] | [];
}
& SearchBarAdvancedPropertiesViewActionProps
& InjectedFormProps & WithStyles<CssRules>;
-const selector = formValueSelector(SEARCH_BAR_ADVANCE_FORM_NAME);
+const selector = formValueSelector(SEARCH_BAR_ADVANCED_FORM_NAME);
const mapStateToProps = (state: RootState) => {
return {
- propertyValues: selector(state, 'key', 'value')
+ propertyValues: selector(state, 'key', 'value', 'keyID', 'valueID')
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
setProps: (propertyValues: PropertyValue[]) => {
- dispatch<any>(changeAdvanceFormProperty('properties', propertyValues));
+ dispatch<any>(changeAdvancedFormProperty('properties', propertyValues));
},
- addProp: (propertyValues: PropertyValue) => {
- dispatch<any>(updateAdvanceFormProperties(propertyValues));
- dispatch<any>(changeAdvanceFormProperty('key'));
- dispatch<any>(changeAdvanceFormProperty('value'));
+ setProp: (propertyValue: PropertyValue, properties: PropertyValue[]) => {
+ dispatch<any>(changeAdvancedFormProperty(
+ 'properties',
+ [...properties.filter(e => e.keyID! !== propertyValue.keyID!), propertyValue]
+ ));
+ dispatch<any>(resetAdvancedFormProperty('key'));
+ dispatch<any>(resetAdvancedFormProperty('value'));
+ dispatch<any>(resetAdvancedFormProperty('keyID'));
+ dispatch<any>(resetAdvancedFormProperty('valueID'));
},
getAllFields: (fields: any) => {
return fields.getAll() || [];
}
});
-export const SearchBarAdvancedPropertiesView = connect(mapStateToProps, mapDispatchToProps)(
+export const SearchBarAdvancedPropertiesView = compose(
+ connectVocabulary,
+ connect(mapStateToProps, mapDispatchToProps))(
withStyles(styles)(
- ({ classes, fields, propertyValues, setProps, addProp, getAllFields }: SearchBarAdvancedPropertiesViewProps) =>
+ ({ classes, fields, propertyValues, setProps, setProp, getAllFields, vocabulary }: SearchBarAdvancedPropertiesViewProps) =>
<Grid container item xs={12} spacing={16}>
<Grid item xs={2} className={classes.label}>Properties</Grid>
<Grid item xs={4}>
<SearchBarValueField />
</Grid>
<Grid container item xs={2} justify='flex-end' alignItems="center">
- <Button className={classes.button} onClick={() => addProp(propertyValues)}
+ <Button className={classes.button} onClick={() => setProp(propertyValues, getAllFields(fields))}
color="primary"
size='small'
variant="contained"
<Chips values={getAllFields(fields)}
deletable
onChange={setProps}
- getLabel={(field: PropertyValue) => formatPropertyValue(field)} />
+ getLabel={(field: PropertyValue) => formatPropertyValue(field, vocabulary)} />
</Grid>
</Grid>
)
import { compose, Dispatch } from 'redux';
import { Paper, StyleRulesCallback, withStyles, WithStyles, Button, Grid, IconButton, CircularProgress } from '@material-ui/core';
import {
- SEARCH_BAR_ADVANCE_FORM_NAME, SEARCH_BAR_ADVANCE_FORM_PICKER_ID,
- searchAdvanceData,
+ SEARCH_BAR_ADVANCED_FORM_NAME, SEARCH_BAR_ADVANCED_FORM_PICKER_ID,
+ searchAdvancedData,
setSearchValueFromAdvancedData
} from '~/store/search-bar/search-bar-actions';
import { ArvadosTheme } from '~/common/custom-theme';
import { CloseIcon } from '~/components/icon/icon';
-import { SearchBarAdvanceFormData } from '~/models/search-bar';
+import { SearchBarAdvancedFormData } from '~/models/search-bar';
import {
SearchBarTypeField, SearchBarClusterField, SearchBarProjectField, SearchBarTrashField,
SearchBarDateFromField, SearchBarDateToField, SearchBarPropertiesField,
};
export const SearchBarAdvancedView = compose(
- reduxForm<SearchBarAdvanceFormData, SearchBarAdvancedViewProps>({
- form: SEARCH_BAR_ADVANCE_FORM_NAME,
+ reduxForm<SearchBarAdvancedFormData, SearchBarAdvancedViewProps>({
+ form: SEARCH_BAR_ADVANCED_FORM_NAME,
validate,
- onSubmit: (data: SearchBarAdvanceFormData, dispatch: Dispatch) => {
- dispatch<any>(searchAdvanceData(data));
- dispatch(reset(SEARCH_BAR_ADVANCE_FORM_NAME));
- dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID }));
+ onSubmit: (data: SearchBarAdvancedFormData, dispatch: Dispatch) => {
+ dispatch<any>(searchAdvancedData(data));
+ dispatch(reset(SEARCH_BAR_ADVANCED_FORM_NAME));
+ dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID }));
},
- onChange: (data: SearchBarAdvanceFormData, dispatch: Dispatch, props: any, prevData: SearchBarAdvanceFormData) => {
+ onChange: (data: SearchBarAdvancedFormData, dispatch: Dispatch, props: any, prevData: SearchBarAdvancedFormData) => {
dispatch<any>(setSearchValueFromAdvancedData(data, prevData));
},
}),
import * as React from 'react';
import { Paper, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
-import { SearchView } from '~/store/search-bar/search-bar-reducer';
import {
SearchBarRecentQueries,
SearchBarRecentQueriesActionProps
import { withStyles, WithStyles, StyleRulesCallback, List, ListItem, ListItemText, ListItemSecondaryAction, Tooltip, IconButton } from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
import { RemoveIcon, EditSavedQueryIcon } from '~/components/icon/icon';
-import { SearchBarAdvanceFormData } from '~/models/search-bar';
+import { SearchBarAdvancedFormData } from '~/models/search-bar';
import { SearchBarSelectedItem } from "~/store/search-bar/search-bar-reducer";
import { getQueryFromAdvancedData } from "~/store/search-bar/search-bar-actions";
});
export interface SearchBarSavedQueriesDataProps {
- savedQueries: SearchBarAdvanceFormData[];
+ savedQueries: SearchBarAdvancedFormData[];
selectedItem: SearchBarSelectedItem;
}
export interface SearchBarSavedQueriesActionProps {
onSearch: (searchValue: string) => void;
deleteSavedQuery: (id: number) => void;
- editSavedQuery: (data: SearchBarAdvanceFormData, id: number) => void;
+ editSavedQuery: (data: SearchBarAdvancedFormData, id: number) => void;
}
type SearchBarSavedQueriesProps = SearchBarSavedQueriesDataProps
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
+import { compose } from 'redux';
import {
IconButton,
Paper,
} from '~/views-components/search-bar/search-bar-advanced-view';
import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "~/common/codes";
import { debounce } from 'debounce';
+import { Vocabulary } from '~/models/vocabulary';
+import { connectVocabulary } from '../resource-properties-form/property-field-common';
type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
currentView: string;
isPopoverOpen: boolean;
debounce?: number;
+ vocabulary?: Vocabulary;
}
export type SearchBarActionProps = SearchBarViewActionProps
loadRecentQueries: () => string[];
moveUp: () => void;
moveDown: () => void;
- setAdvancedDataFromSearchValue: (search: string) => void;
+ setAdvancedDataFromSearchValue: (search: string, vocabulary?: Vocabulary) => void;
}
type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
e.stopPropagation();
- if (props.isPopoverOpen) {
- if (props.currentView === SearchView.ADVANCED) {
- props.closeView();
- } else {
- props.setAdvancedDataFromSearchValue(props.searchValue);
- props.onSetView(SearchView.ADVANCED);
- }
+ if (props.isPopoverOpen && props.currentView === SearchView.ADVANCED) {
+ props.closeView();
} else {
- props.setAdvancedDataFromSearchValue(props.searchValue);
+ props.setAdvancedDataFromSearchValue(props.searchValue, props.vocabulary);
props.onSetView(SearchView.ADVANCED);
}
};
-export const SearchBarView = withStyles(styles)(
+export const SearchBarView = compose(connectVocabulary, withStyles(styles))(
class extends React.Component<SearchBarViewProps> {
debouncedSearch = debounce(() => {
navigateToItem,
editSavedQuery,
changeData,
- submitData, moveUp, moveDown, setAdvancedDataFromSearchValue
+ submitData, moveUp, moveDown, setAdvancedDataFromSearchValue, SEARCH_BAR_ADVANCED_FORM_NAME
} from '~/store/search-bar/search-bar-actions';
import { SearchBarView, SearchBarActionProps, SearchBarDataProps } from '~/views-components/search-bar/search-bar-view';
-import { SearchBarAdvanceFormData } from '~/models/search-bar';
+import { SearchBarAdvancedFormData } from '~/models/search-bar';
+import { Vocabulary } from '~/models/vocabulary';
const mapStateToProps = ({ searchBar, form }: RootState): SearchBarDataProps => {
return {
searchResults: searchBar.searchResults,
selectedItem: searchBar.selectedItem,
savedQueries: searchBar.savedQueries,
- tags: form.searchBarAdvanceFormName,
- saveQuery: form.searchBarAdvanceFormName &&
- form.searchBarAdvanceFormName.values &&
- form.searchBarAdvanceFormName.values.saveQuery
+ tags: form[SEARCH_BAR_ADVANCED_FORM_NAME],
+ saveQuery: form[SEARCH_BAR_ADVANCED_FORM_NAME] &&
+ form[SEARCH_BAR_ADVANCED_FORM_NAME].values &&
+ form[SEARCH_BAR_ADVANCED_FORM_NAME].values!.saveQuery
};
};
deleteSavedQuery: (id: number) => dispatch<any>(deleteSavedQuery(id)),
openSearchView: () => dispatch<any>(openSearchView()),
navigateTo: (uuid: string) => dispatch<any>(navigateToItem(uuid)),
- editSavedQuery: (data: SearchBarAdvanceFormData) => dispatch<any>(editSavedQuery(data)),
+ editSavedQuery: (data: SearchBarAdvancedFormData) => dispatch<any>(editSavedQuery(data)),
moveUp: () => dispatch<any>(moveUp()),
moveDown: () => dispatch<any>(moveDown()),
- setAdvancedDataFromSearchValue: (search: string) => dispatch<any>(setAdvancedDataFromSearchValue(search))
+ setAdvancedDataFromSearchValue: (search: string, vocabulary: Vocabulary) => dispatch<any>(setAdvancedDataFromSearchValue(search, vocabulary))
});
export const SearchBar = connect(mapStateToProps, mapDispatchToProps)(SearchBarView);
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { MenuItem, Select, withStyles, StyleRulesCallback } from '@material-ui/core';
+import { MenuItem, Select } from '@material-ui/core';
import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
import Edit from '@material-ui/icons/Edit';
import Computer from '@material-ui/icons/Computer';
-import { WithStyles } from '@material-ui/core/styles';
import { SelectProps } from '@material-ui/core/Select';
import { SelectItem } from './select-item';
import { PermissionLevel } from '../../models/permission';
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';
+import { ResourceKind } from "~/models/resource";
+import { IllegalNamingWarning } from "~/components/warning/warning";
export interface SidePanelTreeProps {
onItemActivation: (id: string) => void;
sidePanelProgress?: boolean;
(props: SidePanelTreeActionProps) =>
<TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />);
-const renderSidePanelItem = (item: TreeItem<ProjectResource>) =>
- <ListItemTextIcon
+const renderSidePanelItem = (item: TreeItem<ProjectResource>) => {
+ const name = typeof item.data === 'string' ? item.data : item.data.name;
+ const warn = typeof item.data !== 'string' && item.data.kind === ResourceKind.PROJECT
+ ? <IllegalNamingWarning name={name} />
+ : undefined;
+ return <ListItemTextIcon
icon={getProjectPickerIcon(item)}
- name={typeof item.data === 'string' ? item.data : item.data.name}
+ name={name}
+ nameDecorator={warn}
isActive={item.active}
hasMargin={true}
iconSize={1.25}
/>;
+};
const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
typeof item.data === 'string'
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { ArvadosTheme } from '~/common/custom-theme';
import { SidePanelTree, SidePanelTreeProps } from '~/views-components/side-panel-tree/side-panel-tree';
-import { compose, Dispatch } from 'redux';
+import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { navigateFromSidePanel } from '~/store/side-panel/side-panel-action';
import { Grid } from '@material-ui/core';
import * as React from 'react';
import {
StyleRulesCallback, WithStyles, withStyles, Card,
- CardHeader, IconButton, CardContent, Grid, Chip, Tooltip
+ CardHeader, IconButton, CardContent, Grid, Tooltip
} from '@material-ui/core';
import { connect, DispatchProp } from "react-redux";
import { RouteComponentProps } from 'react-router';
import { ArvadosTheme } from '~/common/custom-theme';
import { RootState } from '~/store/store';
-import { MoreOptionsIcon, CollectionIcon, CopyIcon } from '~/components/icon/icon';
+import { MoreOptionsIcon, CollectionIcon } from '~/components/icon/icon';
import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
import { CollectionResource } from '~/models/collection';
import { CollectionPanelFiles } from '~/views-components/collection-panel-files/collection-panel-files';
import { CollectionTagForm } from './collection-tag-form';
import { deleteCollectionTag, navigateToProcess } from '~/store/collection-panel/collection-panel-action';
-import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { getResource } from '~/store/resources/resources';
import { openContextMenu } from '~/store/context-menu/context-menu-actions';
import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
import { formatFileSize } from "~/common/formatters";
-import { getResourceData } from "~/store/resources-data/resources-data";
-import { ResourceData } from "~/store/resources-data/resources-data-reducer";
import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { PropertyChipComponent } from '~/views-components/resource-properties-form/property-chip';
+import { IllegalNamingWarning } from '~/components/warning/warning';
type CssRules = 'card' | 'iconHeader' | 'tag' | 'label' | 'value' | 'link';
interface CollectionPanelDataProps {
item: CollectionResource;
- data: ResourceData;
}
type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
export const CollectionPanel = withStyles(styles)(
connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
const item = getResource(props.match.params.id)(state.resources);
- const data = getResourceData(props.match.params.id)(state.resourcesData);
- return { item, data };
+ return { item };
})(
class extends React.Component<CollectionPanelProps> {
render() {
- const { classes, item, data, dispatch } = this.props;
+ const { classes, item, dispatch } = this.props;
return item
? <>
<Card className={classes.card}>
</IconButton>
</Tooltip>
}
- title={item && item.name}
+ title={item && <span><IllegalNamingWarning name={item.name}/>{item.name}</span>}
titleTypographyProps={this.titleProps}
subheader={item && item.description}
subheaderTypographyProps={this.titleProps} />
label='Portable data hash'
linkToUuid={item && item.portableDataHash} />
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Number of files' value={data && data.fileCount} />
+ label='Number of files' value={item && item.fileCount} />
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Content size' value={data && formatFileSize(data.fileSize)} />
+ label='Content size' value={item && formatFileSize(item.fileSizeTotal)} />
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
label='Owner' linkToUuid={item && item.ownerUuid} />
{(item.properties.container_request || item.properties.containerRequest) &&
<CollectionTagForm />
</Grid>
<Grid item xs={12}>
- {
- Object.keys(item.properties).map(k => {
- return <Chip key={k} className={classes.tag}
- onDelete={this.handleDelete(k)}
- label={`${k}: ${item.properties[k]}`} />;
- })
- }
+ {Object.keys(item.properties).map(k =>
+ <PropertyChipComponent
+ key={k} className={classes.tag}
+ onDelete={this.handleDelete(k)}
+ propKey={k} propValue={item.properties[k]} />
+ )}
</Grid>
</Grid>
</CardContent>
this.props.dispatch<any>(openContextMenu(event, resource));
}
+ onCopy = (message: string) =>
+ this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+ message,
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }))
+
handleDelete = (key: string) => () => {
this.props.dispatch<any>(deleteCollectionTag(key));
}
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { ArvadosTheme } from '~/common/custom-theme';
import { navigateToLinkAccount } from '~/store/navigation/navigation-action';
-
+import { RootState } from '~/store/store';
type CssRules = 'root' | 'ontop' | 'title';
}
});
-type InactivePanelProps = WithStyles<CssRules> & InactivePanelActionProps;
+export interface InactivePanelStateProps {
+ inactivePageText: string;
+}
+
+type InactivePanelProps = WithStyles<CssRules> & InactivePanelActionProps & InactivePanelStateProps;
-export const InactivePanel = connect(null, mapDispatchToProps)(withStyles(styles)((({ classes, startLinking }: InactivePanelProps) =>
- <Grid container justify="center" alignItems="center" direction="column" spacing={24}
- className={classes.root}
- style={{ marginTop: 56, height: "100%" }}>
- <Grid item>
- <Typography variant='h6' align="center" className={classes.title}>
- Hi! You're logged in, but...
- </Typography>
- </Grid>
- <Grid item>
- <Typography align="center">
- Your account is inactive. An administrator must activate your account before you can get any further.
- </Typography>
- </Grid>
- <Grid item>
- <Typography align="center">
- If you would like to use this login to access another account click "Link Account".
- </Typography>
- </Grid>
- <Grid item>
- <Button className={classes.ontop} color="primary" variant="contained" onClick={() => startLinking()}>
- Link Account
- </Button>
- </Grid>
- </Grid >
- )));
+export const InactivePanel = connect((state: RootState) => ({
+ inactivePageText: state.auth.config.clusterConfig.Workbench.InactivePageHTML
+}), mapDispatchToProps)(withStyles(styles)((({ classes, startLinking, inactivePageText }: InactivePanelProps) =>
+ <Grid container justify="center" alignItems="center" direction="column" spacing={24}
+ className={classes.root}
+ style={{ marginTop: 56, height: "100%" }}>
+ <Grid item>
+ <Typography>
+ <div dangerouslySetInnerHTML={{ __html: inactivePageText }} style={{ margin: "1em" }} />
+ </Typography>
+ </Grid>
+ <Grid item>
+ <Typography align="center">
+ If you would like to use this login to access another account click "Link Account".
+ </Typography>
+ </Grid>
+ <Grid item>
+ <Button className={classes.ontop} color="primary" variant="contained" onClick={() => startLinking()}>
+ Link Account
+ </Button>
+ </Grid>
+ </Grid >
+)));
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Button, Typography, Grid, Table, TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton, Checkbox } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Table, TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton, Checkbox } from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
import { MoreOptionsIcon } from '~/components/icon/icon';
import { KeepServiceResource } from '~/models/keep-services';
import { LinkAccountType } from "~/models/link-account";
import { formatDate } from "~/common/formatters";
import { LinkAccountPanelStatus, LinkAccountPanelError } from "~/store/link-account-panel/link-account-panel-reducer";
+import { Config } from '~/common/config';
type CssRules = 'root';
export interface LinkAccountPanelRootDataProps {
targetUser?: UserResource;
userToLink?: UserResource;
- remoteHosts: { [key: string]: string };
+ remoteHostsConfig: { [key: string]: Config };
hasRemoteHosts: boolean;
localCluster: string;
- status : LinkAccountPanelStatus;
+ loginCluster: string;
+ status: LinkAccountPanelStatus;
error: LinkAccountPanelError;
selectedCluster?: string;
isProcessing: boolean;
const disp = [];
disp.push(<span><b>{user.email}</b> ({user.username}, {user.uuid})</span>);
if (showCluster) {
- const homeCluster = user.uuid.substr(0,5);
+ const homeCluster = user.uuid.substr(0, 5);
disp.push(<span> hosted on cluster <b>{homeCluster}</b> and </span>);
}
if (showCreatedAt) {
type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
-export const LinkAccountPanelRoot = withStyles(styles) (
- ({classes, targetUser, userToLink, status, isProcessing, error, startLinking, cancelLinking, linkAccount,
- remoteHosts, hasRemoteHosts, selectedCluster, setSelectedCluster, localCluster}: LinkAccountPanelRootProps) => {
+export const LinkAccountPanelRoot = withStyles(styles)(
+ ({ classes, targetUser, userToLink, status, isProcessing, error, startLinking, cancelLinking, linkAccount,
+ remoteHostsConfig, hasRemoteHosts, selectedCluster, setSelectedCluster, localCluster, loginCluster }: LinkAccountPanelRootProps) => {
return <Card className={classes.root}>
<CardContent>
- { isProcessing && <Grid container item direction="column" alignContent="center" spacing={24}>
- <Grid item>
- Loading user info. Please wait.
- </Grid>
- <Grid item style={{ alignSelf: 'center' }}>
- <CircularProgress/>
- </Grid>
- </Grid> }
- { !isProcessing && status === LinkAccountPanelStatus.INITIAL && targetUser && <div>
- { isLocalUser(targetUser.uuid, localCluster) ? <Grid container spacing={24}>
- <Grid container item direction="column" spacing={24}>
- <Grid item>
- You are currently logged in as {displayUser(targetUser, true)}
- </Grid>
- <Grid item>
- You can link Arvados accounts. After linking, either login will take you to the same account.
- </Grid >
+ {isProcessing && <Grid container item direction="column" alignContent="center" spacing={24}>
+ <Grid item>
+ Loading user info. Please wait.
+ </Grid>
+ <Grid item style={{ alignSelf: 'center' }}>
+ <CircularProgress />
</Grid>
- <Grid container item direction="row" spacing={24}>
- <Grid item>
- <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
- Add another login to this account
- </Button>
- </Grid>
- <Grid item>
- <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
- Use this login to access another account
- </Button>
+ </Grid>}
+ {!isProcessing && status === LinkAccountPanelStatus.INITIAL && targetUser && <div>
+ {isLocalUser(targetUser.uuid, localCluster) ? <Grid container spacing={24}>
+ <Grid container item direction="column" spacing={24}>
+ <Grid item>
+ You are currently logged in as {displayUser(targetUser, true)}
+ </Grid>
+ <Grid item>
+ You can link Arvados accounts. After linking, either login will take you to the same account.
+ </Grid >
</Grid>
- </Grid>
- { hasRemoteHosts && selectedCluster && <Grid container item direction="column" spacing={24}>
- <Grid item>
- You can also link {displayUser(targetUser, false)} with an account from a remote cluster.
+ <Grid container item direction="row" spacing={24}>
+ <Grid item>
+ <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
+ Add another login to this account
+ </Button>
+ </Grid>
+ <Grid item>
+ <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
+ Use this login to access another account
+ </Button>
+ </Grid>
</Grid>
- <Grid item>
- Please select the cluster that hosts the account you want to link with:
- <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
+ {hasRemoteHosts && selectedCluster && <Grid container item direction="column" spacing={24}>
+ <Grid item>
+ You can also link {displayUser(targetUser, false)} with an account from a remote cluster.
+ </Grid>
+ <Grid item>
+ Please select the cluster that hosts the account you want to link with:
+ <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
onChange={(event) => setSelectedCluster(event.target.value)}>
- {Object.keys(remoteHosts).map((k) => k !== localCluster ? <option key={k} value={k}>{k}</option> : null)}
+ {Object.keys(remoteHostsConfig).map((k) => k !== localCluster ? <option key={k} value={k}>{k}</option> : null)}
</Select>
</Grid>
- <Grid item>
- <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT)}>
- Link with an account on {hasRemoteHosts ? <label>{selectedCluster} </label> : null}
- </Button>
- </Grid>
- </Grid> }
- </Grid> :
- <Grid container spacing={24}>
- <Grid container item direction="column" spacing={24}>
- <Grid item>
- You are currently logged in as {displayUser(targetUser, true, true)}
+ <Grid item>
+ <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT)}>
+ Link with an account on {hasRemoteHosts ? <label>{selectedCluster} </label> : null}
+ </Button>
+ </Grid>
+ </Grid>}
+ </Grid> :
+ <Grid container spacing={24}>
+ <Grid container item direction="column" spacing={24}>
+ <Grid item>
+ You are currently logged in as {displayUser(targetUser, true, true)}
+ </Grid>
+ {targetUser.isActive ?
+ (loginCluster === "" ?
+ <> <Grid item>
+ This a remote account. You can link a local Arvados account to this one.
+ After linking, you can access the local account's data by logging into the
+ <b>{localCluster}</b> cluster as user <b>{targetUser.email}</b>
+ from <b>{targetUser.uuid.substr(0, 5)}</b>.
+ </Grid >
+ <Grid item>
+ <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_LOCAL_TO_REMOTE)}>
+ Link an account from {localCluster} to this account
+ </Button>
+ </Grid> </>
+ : <Grid item>Please visit cluster
+ <a href={remoteHostsConfig[loginCluster].workbench2Url + "/link_account"}>{loginCluster}</a>
+ to perform account linking.</Grid>
+ )
+ : <Grid item>
+ This an inactive remote account. An administrator must activate your
+ account before you can proceed. After your accounts is activated,
+ you can link a local Arvados account hosted by the <b>{localCluster}</b>
+ cluster to this one.
+ </Grid >}
+ </Grid>
+ </Grid>}
+ </div>}
+ {!isProcessing && (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && targetUser &&
+ <Grid container spacing={24}>
+ {status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
+ <Grid item>
+ Clicking 'Link accounts' will link {displayUser(userToLink, true, !isLocalUser(targetUser.uuid, localCluster))} to {displayUser(targetUser, true, !isLocalUser(targetUser.uuid, localCluster))}.
+ </Grid>
+ {(isLocalUser(targetUser.uuid, localCluster)) && <Grid item>
+ After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(targetUser)}.
+ </Grid>}
+ <Grid item>
+ Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}.
+ </Grid>
+ {!isLocalUser(targetUser.uuid, localCluster) && <Grid item>
+ You can access <b>{userToLink.email}</b> data by logging into <b>{localCluster}</b> with the <b>{targetUser.email}</b> account.
+ </Grid>}
+ </Grid>}
+ {error === LinkAccountPanelError.NON_ADMIN && <Grid item>
+ Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(targetUser)}.
+ </Grid>}
+ {error === LinkAccountPanelError.SAME_USER && <Grid item>
+ Cannot link {displayUser(targetUser)} to the same account.
+ </Grid>}
+ {error === LinkAccountPanelError.INACTIVE && <Grid item>
+ Cannot link account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}.
+ </Grid>}
+ <Grid container item direction="row" spacing={24}>
+ <Grid item>
+ <Button variant="contained" onClick={() => cancelLinking()}>
+ Cancel
+ </Button>
+ </Grid>
+ <Grid item>
+ <Button disabled={status === LinkAccountPanelStatus.ERROR} color="primary" variant="contained" onClick={() => linkAccount()}>
+ Link accounts
+ </Button>
+ </Grid>
</Grid>
- {targetUser.isActive ? <> <Grid item>
- This a remote account. You can link a local Arvados account to this one. After linking, you can access the local account's data by logging into the <b>{localCluster}</b> cluster as user <b>{targetUser.email}</b> from <b>{targetUser.uuid.substr(0,5)}</b>.
- </Grid >
- <Grid item>
- <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_LOCAL_TO_REMOTE)}>
- Link an account from {localCluster} to this account
- </Button>
- </Grid> </>
- : <Grid item>
- This an inactive remote account. An administrator must activate your account before you can proceed. After your accounts is activated, you can link a local Arvados account hosted by the <b>{localCluster}</b> cluster to this one.
- </Grid >}
- </Grid>
- </Grid>}
- </div> }
- { !isProcessing && (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && targetUser &&
- <Grid container spacing={24}>
- { status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
- <Grid item>
- Clicking 'Link accounts' will link {displayUser(userToLink, true, !isLocalUser(targetUser.uuid, localCluster))} to {displayUser(targetUser, true, !isLocalUser(targetUser.uuid, localCluster))}.
- </Grid>
- { (isLocalUser(targetUser.uuid, localCluster)) && <Grid item>
- After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(targetUser)}.
- </Grid> }
- <Grid item>
- Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}.
- </Grid>
- { !isLocalUser(targetUser.uuid, localCluster) && <Grid item>
- You can access <b>{userToLink.email}</b> data by logging into <b>{localCluster}</b> with the <b>{targetUser.email}</b> account.
- </Grid> }
- </Grid> }
- { error === LinkAccountPanelError.NON_ADMIN && <Grid item>
- Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(targetUser)}.
- </Grid> }
- { error === LinkAccountPanelError.SAME_USER && <Grid item>
- Cannot link {displayUser(targetUser)} to the same account.
- </Grid> }
- { error === LinkAccountPanelError.INACTIVE && <Grid item>
- Cannot link account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}.
- </Grid> }
- <Grid container item direction="row" spacing={24}>
- <Grid item>
- <Button variant="contained" onClick={() => cancelLinking()}>
- Cancel
- </Button>
- </Grid>
- <Grid item>
- <Button disabled={status === LinkAccountPanelStatus.ERROR} color="primary" variant="contained" onClick={() => linkAccount()}>
- Link accounts
- </Button>
- </Grid>
- </Grid>
- </Grid> }
- </CardContent>
- </Card>;
-});
\ No newline at end of file
+ </Grid>}
+ </CardContent>
+ </Card>;
+ });
import { RootState } from '~/store/store';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
-import { startLinking, cancelLinking, linkAccount, linkAccountPanelActions } from '~/store/link-account-panel/link-account-panel-actions';
+import { startLinking, linkAccount, linkAccountPanelActions, cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
import { LinkAccountType } from '~/models/link-account';
import {
LinkAccountPanelRoot,
const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
return {
- remoteHosts: state.auth.remoteHosts,
- hasRemoteHosts: Object.keys(state.auth.remoteHosts).length > 1,
+ remoteHostsConfig: state.auth.remoteHostsConfig,
+ hasRemoteHosts: Object.keys(state.auth.remoteHosts).length > 1 && state.auth.loginCluster === "",
selectedCluster: state.linkAccountPanel.selectedCluster,
localCluster: state.auth.localCluster,
+ loginCluster: state.auth.loginCluster,
targetUser: state.linkAccountPanel.targetUser,
userToLink: state.linkAccountPanel.userToLink,
status: state.linkAccountPanel.status,
startLinking: (type: LinkAccountType) => dispatch<any>(startLinking(type)),
cancelLinking: () => dispatch<any>(cancelLinking(true)),
linkAccount: () => dispatch<any>(linkAccount()),
- setSelectedCluster: (selectedCluster: string) => dispatch<any>(linkAccountPanelActions.SET_SELECTED_CLUSTER({selectedCluster}))
+ setSelectedCluster: (selectedCluster: string) => dispatch<any>(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster }))
});
export const LinkAccountPanel = connect(mapStateToProps, mapDispatchToProps)(LinkAccountPanelRoot);
import * as React from 'react';
import { connect, DispatchProp } from 'react-redux';
-import { Grid, Typography, Button, Select, FormControl } from '@material-ui/core';
+import { Grid, Typography, Button, Select } from '@material-ui/core';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { login, authActions } from '~/store/auth/auth-action';
import { ArvadosTheme } from '~/common/custom-theme';
import { RootState } from '~/store/store';
-import * as classNames from 'classnames';
type CssRules = 'root' | 'container' | 'title' | 'content' | 'content__bolder' | 'button';
left: 0,
bottom: 0,
right: 0,
- background: 'url("arvados-logo-big.png") no-repeat center center',
opacity: 0.2,
}
},
type LoginPanelProps = DispatchProp<any> & WithStyles<CssRules> & {
remoteHosts: { [key: string]: string },
homeCluster: string,
- uuidPrefix: string
+ uuidPrefix: string,
+ loginCluster: string,
+ welcomePage: string
};
export const LoginPanel = withStyles(styles)(
connect((state: RootState) => ({
remoteHosts: state.auth.remoteHosts,
homeCluster: state.auth.homeCluster,
- uuidPrefix: state.auth.localCluster
- }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix }: LoginPanelProps) =>
+ uuidPrefix: state.auth.localCluster,
+ loginCluster: state.auth.loginCluster,
+ welcomePage: state.auth.config.clusterConfig.Workbench.WelcomePageHTML
+ }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix, loginCluster, welcomePage }: LoginPanelProps) =>
<Grid container justify="center" alignItems="center"
className={classes.root}
style={{ marginTop: 56, overflowY: "auto", height: "100%" }}>
<Grid item className={classes.container}>
- <Typography variant='h6' align="center" className={classes.title}>
- Welcome to the Arvados Workbench
- </Typography>
- <Typography className={classes.content}>
- The "Log in" button below will show you a Google sign-in page.
- After you assure Google that you want to log in here with your Google account, you will be redirected back here to Arvados Workbench.
- </Typography>
- <Typography className={classes.content}>
- If you have never used Arvados Workbench before, logging in for the first time will automatically create a new account.
- </Typography>
- <Typography variant='body1' className={classNames(classes.content, classes.content__bolder)}>
- IMPORTANT: Please keep in mind to store exploratory data only but not any information used for clinical decision making.
- </Typography>
- <Typography className={classes.content}>
- Arvados Workbench uses your name and email address only for identification, and does not retrieve any other personal information from Google.
- </Typography>
+ <Typography>
+ <div dangerouslySetInnerHTML={{ __html: welcomePage }} style={{ margin: "1em" }} />
+ </Typography>
+ {Object.keys(remoteHosts).length > 1 && loginCluster === "" &&
- {Object.keys(remoteHosts).length > 1 &&
<Typography component="div" align="right">
<label>Please select the cluster that hosts your user account:</label>
<Select native value={homeCluster} style={{ margin: "1em" }}
<Typography component="div" align="right">
<Button variant="contained" color="primary" style={{ margin: "1em" }} className={classes.button}
- onClick={() => dispatch(login(uuidPrefix, homeCluster, remoteHosts))}>
- Log in to {uuidPrefix}
- {uuidPrefix !== homeCluster &&
- <span> with user from {homeCluster}</span>}
+ onClick={() => dispatch(login(uuidPrefix, homeCluster, loginCluster, remoteHosts))}>
+ Log in
+ {uuidPrefix !== homeCluster && loginCluster !== homeCluster &&
+ <span> to {uuidPrefix} with user from {homeCluster}</span>}
</Button>
</Typography>
</Grid>
import { InactivePanel } from '~/views/inactive-panel/inactive-panel';
import { WorkbenchLoadingScreen } from '~/views/workbench/workbench-loading-screen';
import { MainAppBar } from '~/views-components/main-app-bar/main-app-bar';
-import { LinkAccountPanel } from '~/views/link-account-panel/link-account-panel';
type CssRules = 'root';
uuidPrefix: string;
isNotLinking: boolean;
isLinkingPath: boolean;
+ siteBanner: string;
}
type MainPanelRootProps = MainPanelRootDataProps & WithStyles<CssRules>;
export const MainPanelRoot = withStyles(styles)(
- ({ classes, loading, working, user, buildInfo, uuidPrefix, isNotLinking, isLinkingPath }: MainPanelRootProps) =>
+ ({ classes, loading, working, user, buildInfo, uuidPrefix,
+ isNotLinking, isLinkingPath, siteBanner }: MainPanelRootProps) =>
loading
? <WorkbenchLoadingScreen />
: <>
- { isNotLinking && <MainAppBar
+ {isNotLinking && <MainAppBar
user={user}
buildInfo={buildInfo}
- uuidPrefix={uuidPrefix}>
+ uuidPrefix={uuidPrefix}
+ siteBanner={siteBanner}>
{working ? <LinearProgress color="secondary" /> : null}
- </MainAppBar> }
+ </MainAppBar>}
<Grid container direction="column" className={classes.root}>
- { user ? (user.isActive || (!user.isActive && isLinkingPath) ? <WorkbenchPanel isNotLinking={isNotLinking} isUserActive={user.isActive} /> : <InactivePanel />) : <LoginPanel /> }
+ {user ? (user.isActive || (!user.isActive && isLinkingPath) ? <WorkbenchPanel isNotLinking={isNotLinking} isUserActive={user.isActive} /> : <InactivePanel />) : <LoginPanel />}
</Grid>
</>
);
buildInfo: state.appInfo.buildInfo,
uuidPrefix: state.auth.localCluster,
isNotLinking: state.linkAccountPanel.status === LinkAccountPanelStatus.NONE || state.linkAccountPanel.status === LinkAccountPanelStatus.INITIAL,
- isLinkingPath: state.router.location ? matchLinkAccountRoute(state.router.location.pathname) !== null : false
+ isLinkingPath: state.router.location ? matchLinkAccountRoute(state.router.location.pathname) !== null : false,
+ siteBanner: state.auth.config.clusterConfig.Workbench.SiteName
};
};
} from '~/models/workflow';
import { Field } from 'redux-form';
import { ERROR_MESSAGE } from '~/validators/require';
-import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divider, Grid, WithStyles, Typography } from '@material-ui/core';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divider, WithStyles, Typography } from '@material-ui/core';
import { GenericInputProps, GenericInput } from './generic-input';
import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
import { connect, DispatchProp } from 'react-redux';
} from '~/models/workflow';
import { Field } from 'redux-form';
import { ERROR_MESSAGE } from '~/validators/require';
-import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divider, Grid, WithStyles, Typography } from '@material-ui/core';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divider, WithStyles, Typography } from '@material-ui/core';
import { GenericInputProps, GenericInput } from './generic-input';
import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
import { connect, DispatchProp } from 'react-redux';
import { TextField } from '~/components/text-field/text-field';
import { ExpandIcon } from '~/components/icon/icon';
import * as IntInput from './inputs/int-input';
-import { require } from '~/validators/require';
import { min } from '~/validators/min';
import { optional } from '~/validators/optional';
import { SwitchField } from '~/components/switch-field/switch-field';
export const RUNTIME_FIELD = 'runtime';
export const RAM_FIELD = 'ram';
export const VCPUS_FIELD = 'vcpus';
-export const KEEP_CACHE_RAM_FIELD = 'keepCacheRam';
+export const KEEP_CACHE_RAM_FIELD = 'keep_cache_ram';
export const API_FIELD = 'api';
export interface RunProcessAdvancedFormData {
parse={IntInput.parse}
format={IntInput.format}
type='number'
- validate={keepCacheRamValdation} />
+ validate={keepCacheRamValidation} />
</Grid>
<Grid item xs={12} md={6}>
<Field
const ramValidation = [min(0)];
const vcpusValidation = [min(1)];
-const keepCacheRamValdation = [optional(min(0))];
+const keepCacheRamValidation = [optional(min(0))];
const runtimeValidation = [optional(min(1))];
import { CommandInputParameter, CWLType, IntCommandInputParameter, BooleanCommandInputParameter, FileCommandInputParameter, DirectoryCommandInputParameter, DirectoryArrayCommandInputParameter, FloatArrayCommandInputParameter, IntArrayCommandInputParameter } from '~/models/workflow';
import { IntInput } from '~/views/run-process-panel/inputs/int-input';
import { StringInput } from '~/views/run-process-panel/inputs/string-input';
-import { StringCommandInputParameter, FloatCommandInputParameter, isPrimitiveOfType, File, Directory, WorkflowInputsData, EnumCommandInputParameter, isArrayOfType, StringArrayCommandInputParameter, FileArrayCommandInputParameter } from '../../models/workflow';
+import { StringCommandInputParameter, FloatCommandInputParameter, isPrimitiveOfType, WorkflowInputsData, EnumCommandInputParameter, isArrayOfType, StringArrayCommandInputParameter, FileArrayCommandInputParameter } from '../../models/workflow';
import { FloatInput } from '~/views/run-process-panel/inputs/float-input';
import { BooleanInput } from './inputs/boolean-input';
import { FileInput } from './inputs/file-input';
import { isValid } from 'redux-form';
import { RUN_PROCESS_INPUTS_FORM } from './run-process-inputs-form';
import { RunProcessAdvancedForm, RUN_PROCESS_ADVANCED_FORM } from './run-process-advanced-form';
-import { createSelector, createStructuredSelector } from 'reselect';
+import { createStructuredSelector } from 'reselect';
import { WorkflowPresetSelect } from '~/views/run-process-panel/workflow-preset-select';
import { selectPreset } from '~/store/run-process-panel/run-process-panel-actions';
import { createTree } from '~/models/tree';
import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
import { SearchResultsPanelProps } from "./search-results-panel";
+import { Routes } from '~/routes/routes';
+import { Link } from 'react-router-dom';
+import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
export enum SearchResultsPanelColumnNames {
CLUSTER = "Cluster",
LAST_MODIFIED = "Last modified"
}
+export type CssRules = 'siteManagerLink';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ siteManagerLink: {
+ marginRight: theme.spacing.unit * 2,
+ float: 'right'
+ }
+});
+
export interface WorkflowPanelFilter extends DataTableFilterItem {
type: ResourceKind | ContainerRequestState;
}
}
];
-export const SearchResultsPanelView = (props: SearchResultsPanelProps) => {
- const homeCluster = props.user.uuid.substr(0, 5);
- return <DataExplorer
- id={SEARCH_RESULTS_PANEL_ID}
- onRowClick={props.onItemClick}
- onRowDoubleClick={props.onItemDoubleClick}
- onContextMenu={props.onContextMenu}
- contextMenuColumn={false}
- hideSearchInput
- title={
- props.localCluster === homeCluster ?
- <div>Searching clusters: {props.sessions.filter((ss) => ss.loggedIn).map((ss) => <span key={ss.clusterId}> {ss.clusterId}</span>)}</div> :
- <div>Searching local cluster {props.localCluster} only. To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></div>
- }
- />;
-};
+export const SearchResultsPanelView = withStyles(styles, { withTheme: true })(
+ (props: SearchResultsPanelProps & WithStyles<CssRules, true>) => {
+ const homeCluster = props.user.uuid.substr(0, 5);
+ const loggedIn = props.sessions.filter((ss) => ss.loggedIn);
+ return <DataExplorer
+ id={SEARCH_RESULTS_PANEL_ID}
+ onRowClick={props.onItemClick}
+ onRowDoubleClick={props.onItemDoubleClick}
+ onContextMenu={props.onContextMenu}
+ contextMenuColumn={false}
+ hideSearchInput
+ title={
+ <div>
+ {loggedIn.length === 1 ?
+ <span>Searching local cluster <ResourceCluster uuid={props.localCluster} /></span>
+ : <span>Searching clusters: {loggedIn.map((ss) => <span key={ss.clusterId}>
+ <a href={props.remoteHostsConfig[ss.clusterId] && props.remoteHostsConfig[ss.clusterId].workbench2Url} style={{ textDecoration: 'none' }}> <ResourceCluster uuid={ss.clusterId} /></a>
+ </span>)}</span>}
+ {loggedIn.length === 1 && props.localCluster !== homeCluster ?
+ <span>To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></span>
+ : <span style={{ marginLeft: "2em" }}>Use <Link to={Routes.SITE_MANAGER} >Site Manager</Link> to manage which clusters will be searched.</span>}
+ </div >
+ }
+ />;
+ });
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';
-import { SearchBarAdvanceFormData } from '~/models/search-bar';
+import { SearchBarAdvancedFormData } from '~/models/search-bar';
import { User } from "~/models/user";
import { Config } from '~/common/config';
import { Session } from "~/models/session";
export interface SearchResultsPanelDataProps {
- data: SearchBarAdvanceFormData;
+ data: SearchBarAdvancedFormData;
user: User;
sessions: Session[];
remoteHostsConfig: { [key: string]: Config };
CardContent,
CircularProgress,
Grid,
+ IconButton,
StyleRulesCallback,
Table,
TableBody,
import { TextField } from "~/components/text-field/text-field";
import { addSession } from "~/store/auth/auth-action-session";
import { SITE_MANAGER_REMOTE_HOST_VALIDATION } from "~/validators/validators";
+import { Config } from '~/common/config';
+import { ResourceCluster } from '~/views-components/data-explorer/renderers';
+import { TrashIcon } from "~/components/icon/icon";
type CssRules = 'root' | 'link' | 'buttonContainer' | 'table' | 'tableRow' |
'remoteSiteInfo' | 'buttonAdd' | 'buttonLoggedIn' | 'buttonLoggedOut' |
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
- width: '100%',
- overflow: 'auto'
+ width: '100%',
+ overflow: 'auto'
},
link: {
color: theme.palette.primary.main,
export interface SiteManagerPanelRootActionProps {
toggleSession: (session: Session) => void;
+ removeSession: (session: Session) => void;
}
export interface SiteManagerPanelRootDataProps {
sessions: Session[];
+ remoteHostsConfig: { [key: string]: Config };
+ localClusterConfig: Config;
}
type SiteManagerPanelRootProps = SiteManagerPanelRootDataProps & SiteManagerPanelRootActionProps & WithStyles<CssRules> & InjectedFormProps;
const submitSession = (remoteHost: string) =>
(dispatch: Dispatch) => {
- dispatch<any>(addSession(remoteHost)).then(() => {
+ dispatch<any>(addSession(remoteHost, undefined, true)).then(() => {
dispatch(reset(SITE_MANAGER_FORM_NAME));
}).catch((e: any) => {
const errors = {
};
export const SiteManagerPanelRoot = compose(
- reduxForm<{remoteHost: string}>({
+ reduxForm<{ remoteHost: string }>({
form: SITE_MANAGER_FORM_NAME,
touchOnBlur: false,
onSubmit: (data, dispatch) => {
}
}),
withStyles(styles))
- (({ classes, sessions, handleSubmit, toggleSession }: SiteManagerPanelRootProps) =>
+ (({ classes, sessions, handleSubmit, toggleSession, removeSession, localClusterConfig, remoteHostsConfig }: SiteManagerPanelRootProps) =>
<Card className={classes.root}>
<CardContent>
<Grid container direction="row">
<Grid item xs={12}>
- <Typography paragraph={true} >
+ <Typography paragraph={true} >
You can log in to multiple Arvados sites here, then use the multi-site search page to search collections and projects on all sites at once.
- </Typography>
+ </Typography>
</Grid>
</Grid>
<Grid item xs={12}>
<TableHead>
<TableRow className={classes.tableRow}>
<TableCell>Cluster ID</TableCell>
- <TableCell>Username</TableCell>
+ <TableCell>Host</TableCell>
<TableCell>Email</TableCell>
+ <TableCell>UUID</TableCell>
<TableCell>Status</TableCell>
+ <TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sessions.map((session, index) => {
const validating = session.status === SessionStatus.BEING_VALIDATED;
return <TableRow key={index} className={classes.tableRow}>
- <TableCell>{session.clusterId}</TableCell>
- <TableCell>{validating ? <CircularProgress size={20}/> : session.username}</TableCell>
- <TableCell>{validating ? <CircularProgress size={20}/> : session.email}</TableCell>
+ <TableCell>{remoteHostsConfig[session.clusterId] ?
+ <a href={remoteHostsConfig[session.clusterId].workbench2Url} style={{ textDecoration: 'none' }}> <ResourceCluster uuid={session.clusterId} /></a>
+ : session.clusterId}</TableCell>
+ <TableCell>{session.remoteHost}</TableCell>
+ <TableCell>{validating ? <CircularProgress size={20} /> : session.email}</TableCell>
+ <TableCell>{validating ? <CircularProgress size={20} /> : session.uuid}</TableCell>
<TableCell className={classes.statusCell}>
<Button fullWidth
disabled={validating || session.status === SessionStatus.INVALIDATED || session.active}
{validating ? "Validating" : (session.loggedIn ? "Logged in" : "Logged out")}
</Button>
</TableCell>
+ <TableCell>
+ {session.clusterId !== localClusterConfig.uuidPrefix &&
+ !localClusterConfig.clusterConfig.RemoteClusters[session.clusterId] &&
+ <IconButton onClick={() => removeSession(session)}>
+ <TrashIcon />
+ </IconButton>}
+ </TableCell>
</TableRow>;
})}
</TableBody>
<form onSubmit={handleSubmit}>
<Grid container direction="row">
<Grid item xs={12}>
- <Typography paragraph={true} className={classes.remoteSiteInfo}>
+ <Typography paragraph={true} className={classes.remoteSiteInfo}>
To add a remote Arvados site, paste the remote site's host here (see "ARVADOS_API_HOST" on the "current token" page).
- </Typography>
+ </Typography>
</Grid>
<Grid item xs={8}>
<Field
placeholder="zzzz.arvadosapi.com"
margin="normal"
label="New cluster"
- autoFocus/>
+ autoFocus />
</Grid>
<Grid item xs={3}>
<Button type="submit" variant="contained" color="primary"
SiteManagerPanelRootDataProps
} from "~/views/site-manager-panel/site-manager-panel-root";
import { Session } from "~/models/session";
-import { toggleSession } from "~/store/auth/auth-action-session";
+import { toggleSession, removeSession } from "~/store/auth/auth-action-session";
const mapStateToProps = (state: RootState): SiteManagerPanelRootDataProps => {
return {
- sessions: state.auth.sessions
+ sessions: state.auth.sessions,
+ remoteHostsConfig: state.auth.remoteHostsConfig,
+ localClusterConfig: state.auth.remoteHostsConfig[state.auth.localCluster]
};
};
const mapDispatchToProps = (dispatch: Dispatch): SiteManagerPanelRootActionProps => ({
toggleSession: (session: Session) => {
dispatch<any>(toggleSession(session));
- }
+ },
+ removeSession: (session: Session) => {
+ dispatch<any>(removeSession(session.clusterId));
+ },
});
export const SiteManagerPanel = connect(mapStateToProps, mapDispatchToProps)(SiteManagerPanelRoot);
import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
import { createTree } from '~/models/tree';
import {
- getInitialResourceTypeFilters,
getTrashPanelTypeFilters
} from '~/store/resource-type-filters/resource-type-filters';
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { WithStyles, withStyles, Typography, Tabs, Tab, Paper, Button, Grid } from '@material-ui/core';
+import { WithStyles, withStyles, Tabs, Tab, Paper, Button, Grid } from '@material-ui/core';
import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
import { connect, DispatchProp } from 'react-redux';
import { DataColumns } from '~/components/data-table/data-table';
import * as React from 'react';
import { connect } from 'react-redux';
import { RootState } from '~/store/store';
-import { AuthState } from '~/store/auth/auth-reducer';
import { User } from "~/models/user";
import { getSaltedToken } from '~/store/auth/auth-action-session';
import { Config } from '~/common/config';
if (!apiToken || !user || !user.uuid.startsWith(localCluster)) {
return <></>;
}
- const [, tokenUuid, token] = apiToken.split("/");
return <div id={"fedtoken-iframe-div"}>
{Object.keys(remoteHostsConfig)
.map((k) => {
console.log(`Cluster ${k} does not define workbench2Url. Federated login / cross-site linking to ${k} is unavailable. Tell the admin of ${k} to set Services->Workbench2->ExternalURL in config.yml.`);
return;
}
- return <iframe key={k} src={`${remoteHostsConfig[k].workbench2Url}/fedtoken?api_token=${getSaltedToken(k, tokenUuid, token)}`} style={{
+ const fedtoken = (remoteHostsConfig[k].loginCluster === localCluster)
+ ? apiToken : getSaltedToken(k, apiToken);
+ return <iframe key={k} src={`${remoteHostsConfig[k].workbench2Url}/fedtoken?api_token=${fedtoken}`} style={{
height: 0,
width: 0,
visibility: "hidden"
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { connect } from 'react-redux';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { Route, Switch } from "react-router";
import { ProjectPanel } from "~/views/project-panel/project-panel";
SHARED = "Shared"
}
-const resourceStatus = (type: string) => {
- switch (type) {
- case ResourceStatus.PUBLIC:
- return "Public";
- case ResourceStatus.PRIVATE:
- return "Private";
- case ResourceStatus.SHARED:
- return "Shared";
- default:
- return "Unknown";
- }
-};
+// TODO: restore filters
+// const resourceStatus = (type: string) => {
+// switch (type) {
+// case ResourceStatus.PUBLIC:
+// return "Public";
+// case ResourceStatus.PRIVATE:
+// return "Private";
+// case ResourceStatus.SHARED:
+// return "Shared";
+// default:
+// return "Unknown";
+// }
+// };
export const workflowPanelColumns: DataColumns<string> = [
{
import { FilterBuilder } from "~/services/api/filter-builder";
export const initWebSocket = (config: Config, authService: AuthService, store: RootStore) => {
- const webSocketService = new WebSocketService(config.websocketUrl, authService);
- webSocketService.setMessageListener(messageListener(store));
- webSocketService.connect();
+ if (config.websocketUrl) {
+ const webSocketService = new WebSocketService(config.websocketUrl, authService);
+ webSocketService.setMessageListener(messageListener(store));
+ webSocketService.connect();
+ } else {
+ console.warn("WARNING: Websocket ExternalURL is not set on the cluster config.");
+ }
};
const messageListener = (store: RootStore) => (message: ResourceEventMessage) => {
{
- "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
+ "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier", "tslint-etc"],
"rules": {
"ordered-imports": false,
"member-ordering": false,
"interface-over-type-literal": false,
"no-empty": false,
"no-bitwise": false,
- "ban-types": false
+ "ban-types": false,
+ "no-unused-declaration": true
},
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts",
"src/lib/**",
- "coverage/lcov-report/*.js"
+ "src/**/*.test.ts",
+ "coverage/lcov-report/*.js",
+ "src/common/custom-theme.ts"
]
}
}
"@babel/runtime" "7.0.0"
recompose "^0.29.0"
+"@phenomnomnominal/tsquery@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@phenomnomnominal/tsquery/-/tsquery-3.0.0.tgz#6f2f4dbf6304ff52b12cc7a5b979f20c3794a22a"
+ integrity sha512-SW8lKitBHWJ9fAYkJ9kJivuctwNYCh3BUxLdH0+XiR1GPBiu+7qiZzh8p8jqlj1LgVC1TbvfNFroaEsmYlL8Iw==
+ dependencies:
+ esquery "^1.0.1"
+
+"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0":
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393"
+ integrity sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg==
+ dependencies:
+ type-detect "4.0.8"
+
+"@sinonjs/formatio@^3.2.1":
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c"
+ integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==
+ dependencies:
+ "@sinonjs/commons" "^1"
+ "@sinonjs/samsam" "^3.1.0"
+
+"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.1":
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a"
+ integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==
+ dependencies:
+ "@sinonjs/commons" "^1.3.0"
+ array-from "^2.1.1"
+ lodash "^4.17.15"
+
+"@sinonjs/text-encoding@^0.7.1":
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
+ integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
+
"@types/cheerio@*":
version "0.22.9"
resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.9.tgz#b5990152604c2ada749b7f88cab3476f21f39d7b"
version "1.6.0"
resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.6.0.tgz#537b2949a2ebdcb0d353e448fee45b081021963f"
+"@types/sinon@7.5":
+ version "7.5.1"
+ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c"
+ integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==
+
"@types/uuid@3.4.4":
version "3.4.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"
version "2.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296"
+array-from@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195"
+ integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=
+
array-includes@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
dependencies:
deep-equal "^1.0.1"
-axios@0.18.0:
- version "0.18.0"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"
+axios@0.18.1:
+ version "0.18.1"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3"
+ integrity sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==
dependencies:
- follow-redirects "^1.3.0"
- is-buffer "^1.1.5"
+ follow-redirects "1.5.10"
+ is-buffer "^2.0.2"
babel-code-frame@6.26.0, babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
version "6.26.0"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
+chalk@^2.1.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
change-emitter@^0.1.2:
version "0.1.6"
resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
address "^1.0.1"
debug "^2.6.0"
-diff@^3.2.0:
+diff@^3.2.0, diff@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+esquery@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
+ integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
+ dependencies:
+ estraverse "^4.0.0"
+
esrecurse@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
dependencies:
estraverse "^4.1.0"
+estraverse@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
inherits "^2.0.1"
readable-stream "^2.0.4"
-follow-redirects@^1.0.0, follow-redirects@^1.3.0:
+follow-redirects@1.5.10:
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+ integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+ dependencies:
+ debug "=3.1.0"
+
+follow-redirects@^1.0.0:
version "1.5.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.9.tgz#c9ed9d748b814a39535716e531b9196a845d89c6"
dependencies:
nan "^2.9.2"
node-pre-gyp "^0.10.0"
+fstream@1.0.12:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
+ integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
fstream@^1.0.0, fstream@^1.0.2:
version "1.0.11"
resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
version "1.2.5"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
+handlebars@4.0.14:
+ version "4.0.14"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.14.tgz#88de711eb693a5b783ae06065f9b91b0dd373a71"
+ integrity sha512-E7tDoyAA8ilZIV3xDJgl18sX3M8xB9/fMw8+mfW4msLW8jlX97bAnWgT3pmaNXuvzIEgSBMnAHfuXsB2hdzfow==
+ dependencies:
+ async "^2.5.0"
+ optimist "^0.6.1"
+ source-map "^0.6.1"
+ optionalDependencies:
+ uglify-js "^3.1.4"
+
handlebars@^4.0.3:
version "4.0.12"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5"
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+is-buffer@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"
+ integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==
+
is-builtin-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-js-yaml@3.12.0, js-yaml@^3.10.0, js-yaml@^3.4.3, js-yaml@^3.7.0:
+js-yaml@3.13.1:
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
+ integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+js-yaml@^3.10.0, js-yaml@^3.4.3, js-yaml@^3.7.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
dependencies:
pako "~1.0.2"
readable-stream "~2.0.6"
+just-extend@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc"
+ integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==
+
keycode@^2.1.9:
version "2.2.0"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
p-locate "^2.0.0"
path-exists "^3.0.0"
+lodash-es@4.17.14:
+ version "4.17.14"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.14.tgz#12a95a963cc5955683cee3b74e85458954f37ecc"
+ integrity sha512-7zchRrGa8UZXjD/4ivUWP1867jDkhzTG2c/uj739utSd7O/pFFdxspCemIFKEEjErbcqRzn8nKnGsi7mvTgRPA==
+
lodash-es@^4.17.10, lodash-es@^4.17.5, lodash-es@^4.2.1:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0"
-lodash._reinterpolate@~3.0.0:
+lodash._reinterpolate@^3.0.0, lodash._reinterpolate@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+lodash.mergewith@4.6.2:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
+ integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
+
lodash.mergewith@^4.6.0:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.startswith/-/lodash.startswith-4.2.1.tgz#c598c4adce188a27e53145731cdc6c0e7177600c"
+lodash.template@4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
+ integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
+ dependencies:
+ lodash._reinterpolate "^3.0.0"
+ lodash.templatesettings "^4.0.0"
+
lodash.template@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0"
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
-lodash@4.17.11, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10:
+lodash@4.17.13:
+ version "4.17.13"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93"
+ integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA==
+
+"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+lodash@^4.17.15:
+ version "4.17.15"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+ integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+log-symbols@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
+ integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==
+ dependencies:
+ chalk "^2.0.1"
+
loglevel@^1.4.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
+loglevelnext@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.5.tgz#36fc4f5996d6640f539ff203ba819641680d75a2"
+ integrity sha512-V/73qkPuJmx4BcBF19xPBr+0ZRVBhc4POxvZTZdMeXpJ4NItXSJ/MSwuFT0kQJlCbXvdlZoQQ/418bS1y9Jh6A==
+ dependencies:
+ es6-symbol "^3.1.1"
+ object.assign "^4.1.0"
+
+lolex@^4.0.1, lolex@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7"
+ integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==
+
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
-loud-rejection@^1.0.0:
+loud-rejection@^1.0.0, loud-rejection@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
dependencies:
dependencies:
tmpl "1.0.x"
+map-age-cleaner@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+ integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+ dependencies:
+ p-defer "^1.0.0"
+
map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+mem@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf"
+ integrity sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==
+ dependencies:
+ map-age-cleaner "^0.1.1"
+ mimic-fn "^1.0.0"
+ p-is-promise "^1.1.0"
+
mem@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+mime@^2.1.0:
+ version "2.4.4"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
+ integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
+
mimic-fn@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+nise@^1.4.10:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.2.tgz#b6d29af10e48b321b307e10e065199338eeb2652"
+ integrity sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==
+ dependencies:
+ "@sinonjs/formatio" "^3.2.1"
+ "@sinonjs/text-encoding" "^0.7.1"
+ just-extend "^4.0.2"
+ lolex "^4.1.0"
+ path-to-regexp "^1.7.0"
+
no-case@^2.2.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
+p-defer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+ integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
+
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+p-is-promise@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
+ integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=
+
p-limit@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+set-value@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+ integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
set-value@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+sinon@7.3:
+ version "7.3.2"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.3.2.tgz#82dba3a6d85f6d2181e1eca2c10d8657c2161f28"
+ integrity sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==
+ dependencies:
+ "@sinonjs/commons" "^1.4.0"
+ "@sinonjs/formatio" "^3.2.1"
+ "@sinonjs/samsam" "^3.3.1"
+ diff "^3.5.0"
+ lolex "^4.0.1"
+ nise "^1.4.10"
+ supports-color "^5.5.0"
+
slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
source-map-resolve "^0.5.0"
use "^3.1.0"
+sockjs-client@1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12"
+ integrity sha1-W6vjhrd15M8U51IJEUUmVAFsixI=
+ dependencies:
+ debug "^2.6.6"
+ eventsource "0.1.6"
+ faye-websocket "~0.11.0"
+ inherits "^2.0.1"
+ json3 "^3.3.2"
+ url-parse "^1.1.8"
+
sockjs-client@1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.5.tgz#1bb7c0f7222c40f42adf14f4442cbd1269771a83"
dependencies:
has-flag "^2.0.0"
-supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0:
+supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
dependencies:
loader-utils "^1.0.2"
semver "^5.0.1"
+ts-mock-imports@1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.2.6.tgz#5a98a398c3eadb7f75b6904984bb0ba5f3fbb912"
+ integrity sha512-rZjsIEBWx9a3RGUo4Rhj/hzEGB4GPWJx46fls9EJf4UBsf5SxS2qiozf6dQp0Ym/9LC5MArlXZbZ+93wJzAmjA==
+
tsconfig-paths-webpack-plugin@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-2.0.0.tgz#7652dc684bb3206c8e7e446831ca01cbf4d11772"
version "1.15.0"
resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.15.0.tgz#76b9714399004ab6831fdcf76d89b73691c812cf"
+tslint-etc@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/tslint-etc/-/tslint-etc-1.6.0.tgz#99d1ddf79dc5eaefa14ddbd94742197d0ba0ff45"
+ integrity sha512-+7YkUcHhRowg3odIKV8V4FtrHyf2q/jlabSvn4KjMV+Uansncdq10s0MhFPFCYrSv6Eyhh0vUyu3+T/PcuDO/g==
+ dependencies:
+ "@phenomnomnominal/tsquery" "^3.0.0"
+ tslib "^1.8.0"
+ tsutils "^3.0.0"
+ tsutils-etc "^1.0.0"
+
tslint-react@^3.2.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-3.6.0.tgz#7f462c95c4a0afaae82507f06517ff02942196a1"
tslib "^1.8.0"
tsutils "^2.27.2"
+tsutils-etc@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/tsutils-etc/-/tsutils-etc-1.1.0.tgz#82ce1c92da29e07d3cde95692d5c5e8dbdc92fd0"
+ integrity sha512-pJlLtLmQPUyGHqY/Pq6EGnpGmQCnnTDZetQ7eWkeQ5xaw4GtfcR1Zt7HMKFHGDDp53HzQfbqQ+7ps6iJbfa9Hw==
+
tsutils@^2.13.1, tsutils@^2.27.2:
version "2.29.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
dependencies:
tslib "^1.8.1"
+tsutils@^3.0.0:
+ version "3.17.1"
+ resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
+ integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==
+ dependencies:
+ tslib "^1.8.1"
+
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
dependencies:
prelude-ls "~1.1.2"
+type-detect@4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+ integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
type-is@~1.6.16:
version "1.6.16"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+url-join@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
+ integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
+
url-loader@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+uuid@^3.1.0:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
+ integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
+
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
range-parser "^1.0.3"
time-stamp "^2.0.0"
+webpack-dev-middleware@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.0.1.tgz#7ffd6d0192883c83d3f262e8d7dec822493c6166"
+ integrity sha512-JCturcEZNGA0KHEpOJVRTC/VVazTcPfpR9c1Au6NO9a+jxCRchMi87Qe7y3JeOzc0v5eMMKpuGBnPdN52NA+CQ==
+ dependencies:
+ loud-rejection "^1.6.0"
+ memory-fs "~0.4.1"
+ mime "^2.1.0"
+ path-is-absolute "^1.0.0"
+ range-parser "^1.0.3"
+ url-join "^4.0.0"
+ webpack-log "^1.0.1"
+
webpack-dev-server@2.11.3:
version "2.11.3"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.3.tgz#3fd48a402164a6569d94d3d17f131432631b4873"
webpack-dev-middleware "1.12.2"
yargs "6.6.0"
+webpack-dev-server@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.1.tgz#3c0fdd1ba3b50ebc79858a0e6b9ccdd1565b0c24"
+ integrity sha512-u5lz6REb3+KklgSIytUIOrmWgnpgFmfj/+I+GBXurhEoCsHXpG9twk4NO3bsu72GC9YtxIsiavjfRdhmNt0A/A==
+ dependencies:
+ ansi-html "0.0.7"
+ array-includes "^3.0.3"
+ bonjour "^3.5.0"
+ chokidar "^2.0.0"
+ compression "^1.5.2"
+ connect-history-api-fallback "^1.3.0"
+ debug "^3.1.0"
+ del "^3.0.0"
+ express "^4.16.2"
+ html-entities "^1.2.0"
+ http-proxy-middleware "~0.17.4"
+ import-local "^1.0.0"
+ internal-ip "1.2.0"
+ ip "^1.1.5"
+ killable "^1.0.0"
+ loglevel "^1.4.1"
+ opn "^5.1.0"
+ portfinder "^1.0.9"
+ selfsigned "^1.9.1"
+ serve-index "^1.7.2"
+ sockjs "0.3.19"
+ sockjs-client "1.1.4"
+ spdy "^3.4.1"
+ strip-ansi "^3.0.0"
+ supports-color "^5.1.0"
+ webpack-dev-middleware "3.0.1"
+ webpack-log "^1.1.2"
+ yargs "9.0.1"
+
+webpack-log@^1.0.1, webpack-log@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.2.0.tgz#a4b34cda6b22b518dbb0ab32e567962d5c72a43d"
+ integrity sha512-U9AnICnu50HXtiqiDxuli5gLB5PGBo7VvcHx36jRZHwK4vzOYLbImqT4lwWwoMHdQWwEKw736fCHEekokTEKHA==
+ dependencies:
+ chalk "^2.1.0"
+ log-symbols "^2.1.0"
+ loglevelnext "^1.0.1"
+ uuid "^3.1.0"
+
webpack-manifest-plugin@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-1.3.2.tgz#5ea8ee5756359ddc1d98814324fe43496349a7d4"
y18n "^3.2.1"
yargs-parser "^4.2.0"
+yargs@9.0.1:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c"
+ integrity sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=
+ dependencies:
+ camelcase "^4.1.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ read-pkg-up "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^7.0.0"
+
yargs@^10.0.3:
version "10.1.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5"