const rocheBlue = '#06C';
export const themeOptions: ArvadosThemeOptions = {
+ typography: {
+ useNextVariants: true,
+ },
customs: {
colors: {
green700: green["700"],
dark: teal.A400,
contrastText: '#fff'
}
- }
+ },
};
export const CustomTheme = createMuiTheme(themeOptions);
\ No newline at end of file
onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>) => void;
onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+ onFileClick: (id: string) => void;
}
type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader' | 'uploadIcon' | 'button';
classes={{ action: classes.button }}
action={
<Button onClick={onUploadDataClick}
- variant='raised'
+ variant='contained'
color='primary'
size='small'>
<DownloadIcon className={classes.uploadIcon} />
</DialogContent>
<DialogActions style={{ margin: '0px 24px 24px' }}>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
{props.data.cancelButtonLabel || 'Cancel'}
import { DataExplorer } from "./data-explorer";
import { ColumnSelector } from "../column-selector/column-selector";
-import { DataTable } from "../data-table/data-table";
+import { DataTable, DataTableFetchMode } from "../data-table/data-table";
import { SearchInput } from "../search-input/search-input";
import { TablePagination } from "@material-ui/core";
import { ProjectIcon } from '../icon/icon';
-import { DefaultView } from '../default-view/default-view';
import { SortDirection } from '../data-table/data-column';
configure({ adapter: new Adapter() });
it("communicates with <ColumnSelector/>", () => {
const onColumnToggle = jest.fn();
const onSetColumns = jest.fn();
- const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: [] }];
+ const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: {} }];
const dataExplorer = mount(<DataExplorer
{...mockDataExplorerProps()}
columns={columns}
const onSortToggle = jest.fn();
const onRowClick = jest.fn();
const onSetColumns = jest.fn();
- const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: [] }];
+ const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: {} }];
const items = [{ name: "item 1" }];
const dataExplorer = mount(<DataExplorer
{...mockDataExplorerProps()}
});
const mockDataExplorerProps = () => ({
+ fetchMode: DataTableFetchMode.PAGINATED,
columns: [],
items: [],
itemsAvailable: 0,
onContextMenu: jest.fn(),
defaultIcon: ProjectIcon,
onSetColumns: jest.fn(),
+ onLoadMore: jest.fn(),
defaultMessages: ['testing'],
contextMenuColumn: true
});
import * as React from 'react';
import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip, Button } from '@material-ui/core';
import { ColumnSelector } from "~/components/column-selector/column-selector";
-import { DataTable, DataColumns } from "~/components/data-table/data-table";
+import { DataTable, DataColumns, DataTableFetchMode } from "~/components/data-table/data-table";
import { DataColumn, SortDirection } from "~/components/data-table/data-column";
import { SearchInput } from '~/components/search-input/search-input';
import { ArvadosTheme } from "~/common/custom-theme";
});
interface DataExplorerDataProps<T> {
+ fetchMode: DataTableFetchMode;
items: T[];
itemsAvailable: number;
columns: DataColumns<T>;
onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
onChangePage: (page: number) => void;
onChangeRowsPerPage: (rowsPerPage: number) => void;
+ onLoadMore: (page: number) => void;
extractKey?: (item: T) => React.Key;
}
rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
- paperKey
+ paperKey, fetchMode
} = this.props;
return <Paper className={classes.root} {...paperProps} key={paperKey}>
- <Toolbar className={classes.toolbar}>
+ {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={classes.toolbar}>
<Grid container justify="space-between" wrap="nowrap" alignItems="center">
{!hideSearchInput && <div className={classes.searchBox}>
<SearchInput
columns={columns}
onColumnToggle={onColumnToggle} />}
</Grid>
- </Toolbar>
+ </Toolbar>}
<DataTable
columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
items={items}
defaultView={dataTableDefaultView} />
<Toolbar className={classes.footer}>
<Grid container justify="flex-end">
- <TablePagination
+ {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
count={itemsAvailable}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={rowsPerPageOptions}
page={this.props.page}
onChangePage={this.changePage}
onChangeRowsPerPage={this.changeRowsPerPage}
- component="div" />
+ component="div" /> : <Button
+ variant="text"
+ size="medium"
+ onClick={this.loadMore}
+ >Load more</Button>}
</Grid>
</Toolbar>
</Paper>;
this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
}
+ loadMore = () => {
+ this.props.onLoadMore(this.props.page + 1);
+ }
+
renderContextMenuTrigger = (item: T) =>
<Grid container justify="center">
<Tooltip title="More options" disableFocusListener>
name: "Actions",
selected: true,
configurable: false,
- sortDirection: SortDirection.NONE,
filters: createTree(),
key: "context-actions",
render: this.renderContextMenuTrigger
<CardActions>
<Button
color="primary"
- variant="raised"
+ variant='contained'
size="small"
onClick={this.submit}>
Ok
<CardActions>
<Button
color="primary"
- variant="raised"
+ variant='contained'
size="small"
onClick={this.submit}>
Ok
export type DataColumns<T> = Array<DataColumn<T>>;
+export enum DataTableFetchMode {
+ PAGINATED,
+ INFINITE
+}
+
export interface DataTableDataProps<T> {
items: T[];
columns: DataColumns<T>;
}
},
typography: {
- fontFamily: 'monospace'
+ fontFamily: 'monospace',
+ useNextVariants: true,
}
});
<Typography className={classnames([classes.root, classRoot])} component="div">
<Icon className={classnames([classes.icon, classIcon])} />
{messages.map((msg: string, index: number) => {
- return <Typography key={index} variant="body1"
+ return <Typography key={index}
className={classnames([classes.message, classMessage])}>{msg}</Typography>;
})}
</Typography>
return <>
<div className={classes.root}>
<ListItemTextIcon
- icon={getIcon(item)}
+ icon={getIcon(item.data.type)}
name={item.data.name} />
<div className={classes.spacer} />
<Typography
</IconButton>
</Tooltip>
</div >
- <FileThumbnail file={item.data} />
</>;
}
}
});
-const getIcon = (item: TreeItem<FileTreeData>) => {
- switch (item.data.type) {
+export const getIcon = (type: string) => {
+ switch (type) {
case 'directory':
return ProjectIcon;
case 'file':
onMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+ onFileClick: (id: string) => void;
}
export class FileTree extends React.Component<FileTreeProps> {
this.props.onCollapseToggle(id, status);
}
- handleToggleActive = () => { return; };
+ handleToggleActive = (_: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => {
+ this.props.onFileClick(item.id);
+ }
handleSelectionChange = (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => {
event.stopPropagation();
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
disabled={props.uploading}
onClick={props.closeDialog}>
{files.length === 0 &&
<Grid container justify="center" alignItems="center" className={classes.container}>
<Grid item component={"span"}>
- <Typography variant={"subheading"}>
+ <Typography variant='subtitle1'>
<CloudUploadIcon className={classes.uploadIcon} /> Drag and drop data or click to browse
</Typography>
</Grid>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WrappedFieldProps, WrappedFieldInputProps } from 'redux-form';
+import { FormGroup, FormLabel, FormHelperText } from '@material-ui/core';
+
+interface FormFieldCustomProps {
+ children: <P>(props: WrappedFieldInputProps) => React.ReactElement<P>;
+ label?: string;
+ helperText?: string;
+ required?: boolean;
+}
+
+export type FormFieldProps = FormFieldCustomProps & WrappedFieldProps;
+
+export const FormField = ({ children, ...props }: FormFieldProps & WrappedFieldProps) => {
+ return (
+ <FormGroup>
+
+ <FormLabel
+ focused={props.meta.active}
+ required={props.required}
+ error={props.meta.touched && !!props.meta.error}>
+ {props.label}
+ </FormLabel>
+
+ { children(props.input) }
+
+ <FormHelperText error={props.meta.touched && !!props.meta.error}>
+ {
+ props.meta.touched && props.meta.error
+ ? props.meta.error
+ : props.helperText
+ }
+ </FormHelperText>
+
+ </FormGroup>
+ );
+};
<Icon style={{ fontSize: `${iconSize}rem` }} />
</ListItemIcon>
<ListItemText primary={
- <Typography variant='body1' className={classnames(classes.listItemText, {
+ <Typography className={classnames(classes.listItemText, {
[classes.active]: isActive
})}>
{name}
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
disabled={props.submitting}
onClick={props.closeDialog}>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { FormFieldProps, FormField } from '~/components/form-field/form-field';
+import { Switch } from '@material-ui/core';
+import { SwitchProps } from '@material-ui/core/Switch';
+
+export const SwitchField = ({ switchProps, ...props }: FormFieldProps & { switchProps: SwitchProps }) =>
+ <FormField {...props}>
+ {input => <Switch {...switchProps} checked={input.value} onChange={input.onChange} />}
+ </FormField>;
+
type TextFieldProps = WrappedFieldProps & WithStyles<CssRules>;
export const TextField = withStyles(styles)((props: TextFieldProps & {
- label?: string, autoFocus?: boolean, required?: boolean, select?: boolean, disabled?: boolean, children: React.ReactNode, margin?: Margin, placeholder?: string
+ label?: string, autoFocus?: boolean, required?: boolean, select?: boolean, disabled?: boolean, children: React.ReactNode, margin?: Margin, placeholder?: string,
+ helperText?: string, type?: string,
}) =>
<MaterialTextField
- helperText={props.meta.touched && props.meta.error}
+ helperText={(props.meta.touched && props.meta.error) || props.helperText}
className={props.classes.textField}
label={props.label}
disabled={props.disabled || props.meta.submitting}
children={props.children}
margin={props.margin}
placeholder={props.placeholder}
+ type={props.type}
{...props.input}
/>);
//
// SPDX-License-Identifier: AGPL-3.0
-export interface ApiClientAuthorization {
+import { Resource } from '~/models/resource';
+
+export interface ApiClientAuthorization extends Resource {
uuid: string;
apiToken: string;
apiClientId: number;
import { CollectionResource } from "./collection";
import { ProcessResource } from "./process";
import { EmptyResource } from "./empty";
+import { CollectionFile, CollectionDirectory } from '~/models/collection-file';
-export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource;
+export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource | CollectionFile | CollectionDirectory;
// SPDX-License-Identifier: AGPL-3.0
export interface SchedulingParameters {
- partitions: string[];
- preemptible: boolean;
- maxRunTime: number;
+ partitions?: string[];
+ preemptible?: boolean;
+ maxRunTime?: number;
}
export type SearchBarAdvanceFormData = {
type?: ResourceKind;
- cluster?: ClusterObjectType;
+ cluster?: string;
projectUuid?: string;
inTrash: boolean;
dateFrom: string;
key: string;
value: string;
}
-
-export enum ClusterObjectType {
- INDIANAPOLIS = "indianapolis",
- KAISERAUGST = "kaiseraugst",
- PENZBERG = "penzberg"
-}
};
const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
+
const rootMatch = Routes.matchRootRoute(pathname);
const projectMatch = Routes.matchProjectRoute(pathname);
const collectionMatch = Routes.matchCollectionRoute(pathname);
} else if (linksMatch) {
store.dispatch(WorkbenchActions.loadLinks);
}
-};
+};
\ No newline at end of file
it("should build correct order query", () => {
const order = new OrderBuilder()
.addAsc("kind")
- .addDesc("modifiedAt")
+ .addDesc("createdAt")
.getOrder();
- expect(order).toEqual("kind asc,modified_at desc");
+ expect(order).toEqual("kind asc,created_at desc");
});
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { getUserFullname, User, UserPrefs } from "~/models/user";
+import { getUserFullname, User, UserPrefs, UserResource } from '~/models/user';
import { AxiosInstance } from "axios";
import { ApiActions } from "~/services/api/api-actions";
import * as uuid from "uuid/v4";
: undefined;
}
- public saveUser(user: User) {
+ 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);
//
// SPDX-License-Identifier: AGPL-3.0
-import * as _ from "lodash";
import { AxiosInstance } from "axios";
import { Resource } from "src/models/resource";
import { ApiActions } from "~/services/api/api-actions";
}
export class CommonResourceService<T extends Resource> extends CommonService<T> {
-
- constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
+ constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
super(serverApi, resourceType, actions);
}
-
}
export const getCommonResourceServiceError = (errorResponse: any) => {
}
export interface ListResults<T> {
+ clusterId?: string;
kind: string;
offset: number;
limit: number;
this.actions
);
}
-}
\ No newline at end of file
+}
import * as _ from "lodash";
import { CommonResourceService } from '~/services/common-service/common-resource-service';
import { ListResults, ListArguments } from '~/services/common-service/common-service';
-import { AxiosInstance } from "axios";
+import { AxiosInstance, AxiosRequestConfig } from "axios";
import { CollectionResource } from "~/models/collection";
import { ProjectResource } from "~/models/project";
import { ProcessResource } from "~/models/process";
import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
import { ApiActions } from "~/services/api/api-actions";
import { GroupResource } from "~/models/group";
+import { Session } from "~/models/session";
export interface ContentsArguments {
limit?: number;
super(serverApi, "groups", actions);
}
- async contents(uuid: string, args: ContentsArguments = {}): Promise<ListResults<GroupContentsResource>> {
+ async contents(uuid: string, args: ContentsArguments = {}, session?: Session): Promise<ListResults<GroupContentsResource>> {
const { filters, order, ...other } = args;
const params = {
...other,
};
const pathUrl = uuid ? `${uuid}/contents` : 'contents';
+
+ const cfg: AxiosRequestConfig = { params: CommonResourceService.mapKeys(_.snakeCase)(params) };
+ if (session) {
+ cfg.baseURL = session.baseUrl;
+ }
+
const response = await CommonResourceService.defaultResponse(
- this.serverApi
- .get(this.resourceType + pathUrl, {
- params: CommonResourceService.mapKeys(_.snakeCase)(params)
- }),
- this.actions,
- false
- );
+ this.serverApi.get(this.resourceType + pathUrl, cfg), this.actions, false
+ );
const { items, ...res } = response;
- const mappedItems = items.map((item: GroupContentsResource) => {
+ const mappedItems = (items || []).map((item: GroupContentsResource) => {
const mappedItem = TrashableResourceService.mapKeys(_.camelCase)(item);
if (item.kind === ResourceKind.COLLECTION || item.kind === ResourceKind.PROJECT) {
const { properties } = item;
}
});
const mappedResponse = { ...TrashableResourceService.mapKeys(_.camelCase)(res) };
- return { ...mappedResponse, items: mappedItems };
+ return { ...mappedResponse, items: mappedItems, clusterId: session && session.clusterId };
}
shared(params: SharedArguments = {}): Promise<ListResults<GroupContentsResource>> {
dispatch<any>(initAdvancedTabDialog(advanceDataComputeNode));
break;
case ResourceKind.API_CLIENT_AUTHORIZATION:
- const dataApiClientAuthorization = getState().apiClientAuthorizations.find(item => item.uuid === uuid);
+ const apiClientAuthorizationResources = getState().resources;
+ const dataApiClientAuthorization = getResource<ApiClientAuthorization>(uuid)(apiClientAuthorizationResources);
const advanceDataApiClientAuthorization = advancedTabData({
uuid,
metadata: '',
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from "redux";
-import { unionize, ofType, UnionOf } from "~/common/unionize";
import { RootState } from '~/store/store';
import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
import { ServiceRepository } from "~/services/services";
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
import { navigateToRootProject } from '~/store/navigation/navigation-action';
import { ApiClientAuthorization } from '~/models/api-client-authorization';
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { getResource } from '~/store/resources/resources';
-export const apiClientAuthorizationsActions = unionize({
- SET_API_CLIENT_AUTHORIZATIONS: ofType<ApiClientAuthorization[]>(),
- REMOVE_API_CLIENT_AUTHORIZATION: ofType<string>()
-});
-export type ApiClientAuthorizationsActions = UnionOf<typeof apiClientAuthorizationsActions>;
+export const API_CLIENT_AUTHORIZATION_PANEL_ID = 'apiClientAuthorizationPanelId';
+export const apiClientAuthorizationsActions = bindDataExplorerActions(API_CLIENT_AUTHORIZATION_PANEL_ID);
export const API_CLIENT_AUTHORIZATION_REMOVE_DIALOG = 'apiClientAuthorizationRemoveDialog';
export const API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG = 'apiClientAuthorizationAttributesDialog';
export const API_CLIENT_AUTHORIZATION_HELP_DIALOG = 'apiClientAuthorizationHelpDialog';
+
export const loadApiClientAuthorizationsPanel = () =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const user = getState().auth.user;
if (user && user.isAdmin) {
try {
dispatch(setBreadcrumbs([{ label: 'Api client authorizations' }]));
- const response = await services.apiClientAuthorizationService.list();
- dispatch(apiClientAuthorizationsActions.SET_API_CLIENT_AUTHORIZATIONS(response.items));
+ dispatch(apiClientAuthorizationsActions.REQUEST_ITEMS());
} catch (e) {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000 }));
return;
}
} else {
export const openApiClientAuthorizationAttributesDialog = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
- const apiClientAuthorization = getState().apiClientAuthorizations.find(node => node.uuid === uuid);
+ const { resources } = getState();
+ const apiClientAuthorization = getResource<ApiClientAuthorization>(uuid)(resources);
dispatch(dialogActions.OPEN_DIALOG({ id: API_CLIENT_AUTHORIZATION_ATTRIBUTES_DIALOG, data: { apiClientAuthorization } }));
};
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
try {
await services.apiClientAuthorizationService.delete(uuid);
- dispatch(apiClientAuthorizationsActions.REMOVE_API_CLIENT_AUTHORIZATION(uuid));
+ dispatch(apiClientAuthorizationsActions.REQUEST_ITEMS());
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Api client authorization has been successfully removed.', hideDuration: 2000 }));
} catch (e) {
return;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateResources } from '~/store/resources/resources-actions';
+import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { apiClientAuthorizationsActions } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { ListResults } from '~/services/common-service/common-service';
+import { ApiClientAuthorization } from '~/models/api-client-authorization';
+import { ApiClientAuthorizationPanelColumnNames } from '~/views/api-client-authorization-panel/api-client-authorization-panel-root';
+import { SortDirection } from '~/components/data-table/data-column';
+
+export class ApiClientAuthorizationMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const state = api.getState();
+ const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+ try {
+ const response = await this.services.apiClientAuthorizationService.list(getParams(dataExplorer));
+ api.dispatch(updateResources(response.items));
+ api.dispatch(setItems(response));
+ } catch {
+ api.dispatch(couldNotFetchLinks());
+ }
+ }
+}
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+ ...dataExplorerToListParams(dataExplorer),
+ order: getOrder(dataExplorer)
+});
+
+const getOrder = (dataExplorer: DataExplorer) => {
+ const sortColumn = getSortColumn(dataExplorer);
+ const order = new OrderBuilder<ApiClientAuthorization>();
+ if (sortColumn) {
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+
+ const columnName = sortColumn && sortColumn.name === ApiClientAuthorizationPanelColumnNames.UUID ? "uuid" : "updatedAt";
+ return order
+ .addOrder(sortDirection, columnName)
+ .getOrder();
+ } else {
+ return order.getOrder();
+ }
+};
+
+export const setItems = (listResults: ListResults<ApiClientAuthorization>) =>
+ apiClientAuthorizationsActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(listResults),
+ items: listResults.items.map(resource => resource.uuid),
+ });
+
+const couldNotFetchLinks = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch api client authorizations.',
+ kind: SnackbarKind.ERROR
+ });
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import {
- apiClientAuthorizationsActions,
- ApiClientAuthorizationsActions
-} from '~/store/api-client-authorizations/api-client-authorizations-actions';
-import { ApiClientAuthorization } from '~/models/api-client-authorization';
-
-export type ApiClientAuthorizationsState = ApiClientAuthorization[];
-
-const initialState: ApiClientAuthorizationsState = [];
-
-export const apiClientAuthorizationsReducer =
- (state: ApiClientAuthorizationsState = initialState, action: ApiClientAuthorizationsActions): ApiClientAuthorizationsState =>
- apiClientAuthorizationsActions.match(action, {
- SET_API_CLIENT_AUTHORIZATIONS: apiClientAuthorizations => apiClientAuthorizations,
- REMOVE_API_CLIENT_AUTHORIZATION: (uuid: string) =>
- state.filter((apiClientAuthorization) => apiClientAuthorization.uuid !== uuid),
- default: () => state
- });
\ No newline at end of file
return Promise.resolve(uuid);
}
- const resp = await Axios.get(`${baseUrl}/api_client_authorizations`, {
+ const resp = await Axios.get(`${baseUrl}api_client_authorizations`, {
headers: {
Authorization: `OAuth2 ${token}`
},
return `v2/${tokenUuid}/${hmac}`;
};
-const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{user: User, token: string}> => {
+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);
};
};
-const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
+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);
export const loadSiteManagerPanel = () =>
async (dispatch: Dispatch<any>) => {
try {
- dispatch(setBreadcrumbs([{ label: 'Site Manager'}]));
+ dispatch(setBreadcrumbs([{ label: 'Site Manager' }]));
dispatch(validateSessions());
} catch (e) {
return;
};
export const openApiClientAuthorizationContextMenu =
- (event: React.MouseEvent<HTMLElement>, apiClientAuthorization: ApiClientAuthorization) =>
+ (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
(dispatch: Dispatch) => {
dispatch<any>(openContextMenu(event, {
name: '',
- uuid: apiClientAuthorization.uuid,
- ownerUuid: apiClientAuthorization.ownerUuid,
+ uuid: resourceUuid,
+ ownerUuid: '',
kind: ResourceKind.API_CLIENT_AUTHORIZATION,
menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION
}));
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from "~/common/unionize";
-import { DataColumns } from "~/components/data-table/data-table";
+import { DataColumns, DataTableFetchMode } from "~/components/data-table/data-table";
import { DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree';
export const dataExplorerActions = unionize({
+ CLEAR: ofType<{ id: string }>(),
RESET_PAGINATION: ofType<{ id: string }>(),
- REQUEST_ITEMS: ofType<{ id: string }>(),
+ REQUEST_ITEMS: ofType<{ id: string, criteriaChanged?: boolean }>(),
+ SET_FETCH_MODE: ofType<({ id: string, fetchMode: DataTableFetchMode })>(),
SET_COLUMNS: ofType<{ id: string, columns: DataColumns<any> }>(),
SET_FILTERS: ofType<{ id: string, columnName: string, filters: DataTableFilters }>(),
SET_ITEMS: ofType<{ id: string, items: any[], page: number, rowsPerPage: number, itemsAvailable: number }>(),
+ APPEND_ITEMS: ofType<{ id: string, items: any[], page: number, rowsPerPage: number, itemsAvailable: number }>(),
SET_PAGE: ofType<{ id: string, page: number }>(),
SET_ROWS_PER_PAGE: ofType<{ id: string, rowsPerPage: number }>(),
TOGGLE_COLUMN: ofType<{ id: string, columnName: string }>(),
export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
export const bindDataExplorerActions = (id: string) => ({
+ CLEAR: () =>
+ dataExplorerActions.CLEAR({ id }),
RESET_PAGINATION: () =>
dataExplorerActions.RESET_PAGINATION({ id }),
- REQUEST_ITEMS: () =>
- dataExplorerActions.REQUEST_ITEMS({ id }),
+ REQUEST_ITEMS: (criteriaChanged?: boolean) =>
+ dataExplorerActions.REQUEST_ITEMS({ id, criteriaChanged }),
+ SET_FETCH_MODE: (payload: { fetchMode: DataTableFetchMode }) =>
+ dataExplorerActions.SET_FETCH_MODE({ ...payload, id }),
SET_COLUMNS: (payload: { columns: DataColumns<any> }) =>
dataExplorerActions.SET_COLUMNS({ ...payload, id }),
SET_FILTERS: (payload: { columnName: string, filters: DataTableFilters }) =>
dataExplorerActions.SET_FILTERS({ ...payload, id }),
SET_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) =>
dataExplorerActions.SET_ITEMS({ ...payload, id }),
+ APPEND_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) =>
+ dataExplorerActions.APPEND_ITEMS({ ...payload, id }),
SET_PAGE: (payload: { page: number }) =>
dataExplorerActions.SET_PAGE({ ...payload, id }),
SET_ROWS_PER_PAGE: (payload: { rowsPerPage: number }) =>
return getDataExplorerColumnFilters(columns, columnName);
}
- abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
+ abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean): void;
}
export const getDataExplorerColumnFilters = <T>(columns: DataColumns<T>, columnName: string): DataTableFilters => {
middleware(dataExplorerActions.SET_PAGE({ id: "OtherId", page: 0 }));
middleware(dataExplorerActions.SET_PAGE({ id: "ServiceId", page: 0 }));
middleware(dataExplorerActions.SET_PAGE({ id: "OtherId", page: 0 }));
- expect(api.dispatch).toHaveBeenCalledWith(dataExplorerActions.REQUEST_ITEMS({ id: "ServiceId" }));
+ expect(api.dispatch).toHaveBeenCalledWith(dataExplorerActions.REQUEST_ITEMS({ id: "ServiceId", criteriaChanged: false }));
expect(api.dispatch).toHaveBeenCalledTimes(1);
});
};
dataExplorerActions.match(action, {
SET_PAGE: handleAction(() => {
- api.dispatch(actions.REQUEST_ITEMS());
+ api.dispatch(actions.REQUEST_ITEMS(false));
}),
SET_ROWS_PER_PAGE: handleAction(() => {
- api.dispatch(actions.REQUEST_ITEMS());
+ api.dispatch(actions.REQUEST_ITEMS(true));
}),
SET_FILTERS: handleAction(() => {
api.dispatch(actions.RESET_PAGINATION());
- api.dispatch(actions.REQUEST_ITEMS());
+ api.dispatch(actions.REQUEST_ITEMS(true));
}),
TOGGLE_SORT: handleAction(() => {
- api.dispatch(actions.REQUEST_ITEMS());
+ api.dispatch(actions.REQUEST_ITEMS(true));
}),
SET_EXPLORER_SEARCH_VALUE: handleAction(() => {
api.dispatch(actions.RESET_PAGINATION());
- api.dispatch(actions.REQUEST_ITEMS());
+ api.dispatch(actions.REQUEST_ITEMS(true));
}),
- REQUEST_ITEMS: handleAction(() => {
- service.requestItems(api);
+ REQUEST_ITEMS: handleAction(({ criteriaChanged }) => {
+ service.requestItems(api, criteriaChanged);
}),
default: () => next(action)
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { DataColumn, toggleSortDirection, resetSortDirection, SortDirection } from "~/components/data-table/data-column";
-import { dataExplorerActions, DataExplorerAction } from "./data-explorer-action";
-import { DataColumns } from "~/components/data-table/data-table";
+import {
+ DataColumn,
+ resetSortDirection,
+ SortDirection,
+ toggleSortDirection
+} from "~/components/data-table/data-column";
+import { DataExplorerAction, dataExplorerActions } from "./data-explorer-action";
+import { DataColumns, DataTableFetchMode } from "~/components/data-table/data-table";
import { DataTableFilters } from "~/components/data-table-filters/data-table-filters-tree";
export interface DataExplorer {
+ fetchMode: DataTableFetchMode;
columns: DataColumns<any>;
items: any[];
itemsAvailable: number;
}
export const initialDataExplorer: DataExplorer = {
+ fetchMode: DataTableFetchMode.PAGINATED,
columns: [],
items: [],
itemsAvailable: 0,
export const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
dataExplorerActions.match(action, {
+ CLEAR: ({ id }) =>
+ update(state, id, explorer => ({ ...explorer, page: 0, itemsAvailable: 0, items: [] })),
+
RESET_PAGINATION: ({ id }) =>
update(state, id, explorer => ({ ...explorer, page: 0 })),
+ SET_FETCH_MODE: ({ id, fetchMode }) =>
+ update(state, id, explorer => ({ ...explorer, fetchMode })),
+
SET_COLUMNS: ({ id, columns }) =>
update(state, id, setColumns(columns)),
SET_ITEMS: ({ id, items, itemsAvailable, page, rowsPerPage }) =>
update(state, id, explorer => ({ ...explorer, items, itemsAvailable, page, rowsPerPage })),
+ APPEND_ITEMS: ({ id, items, itemsAvailable, page, rowsPerPage }) =>
+ update(state, id, explorer => ({
+ ...explorer,
+ items: state[id].items.concat(items),
+ itemsAvailable: state[id].itemsAvailable + itemsAvailable,
+ page,
+ rowsPerPage
+ })),
+
SET_PAGE: ({ id, page }) =>
update(state, id, explorer => ({ ...explorer, page })),
export const detailsPanelActions = unionize({
TOGGLE_DETAILS_PANEL: ofType<{}>(),
+ OPEN_DETAILS_PANEL: ofType<string>(),
LOAD_DETAILS_PANEL: ofType<string>()
});
export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
+export const openDetailsPanel = (uuid: string) => detailsPanelActions.OPEN_DETAILS_PANEL(uuid);
+
export const openProjectPropertiesDialog = () =>
(dispatch: Dispatch) => {
dispatch<any>(dialogActions.OPEN_DIALOG({ id: PROJECT_PROPERTIES_DIALOG_NAME, data: { } }));
detailsPanelActions.match(action, {
default: () => state,
LOAD_DETAILS_PANEL: resourceUuid => ({ ...state, resourceUuid }),
- TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
+ OPEN_DETAILS_PANEL: resourceUuid => ({ resourceUuid, isOpened: true }),
+ TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened }),
});
initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
);
+export const getTrashPanelTypeFilters = pipe(
+ (): DataTableFilters => createTree<DataTableFilterItem>(),
+ initFilter(ObjectTypeFilter.PROJECT),
+ initFilter(ObjectTypeFilter.COLLECTION),
+ initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION),
+ initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION),
+ initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
+);
const createFiltersBuilder = (filters: DataTableFilters) =>
({ fb: new FilterBuilder(), selectedFilters: getSelectedNodes(filters) });
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from '~/common/unionize';
-import { Resource, extractUuidKind } from '~/models/resource';
+import { extractUuidKind, Resource } from '~/models/resource';
import { Dispatch } from 'redux';
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { createWorkflowMounts } from '~/models/process';
import { ContainerRequestState } from '~/models/container-request';
import { navigateToProcess } from '../navigation/navigation-action';
-import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from '~/views/run-process-panel/run-process-advanced-form';
+import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM, VCPUS_FIELD, RAM_FIELD, RUNTIME_FIELD, OUTPUT_FIELD, API_FIELD } from '~/views/run-process-panel/run-process-advanced-form';
import { isItemNotInProject, isProjectOrRunProcessRoute } from '~/store/projects/project-create-actions';
import { dialogActions } from '~/store/dialog/dialog-actions';
import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
dispatch(runProcessPanelActions.SET_STEP_CHANGED(false));
dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
dispatch<any>(loadPresets(workflow.uuid));
+ dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, DEFAULT_ADVANCED_FORM_VALUES));
}
if (!isWorkflowChanged) {
dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
dispatch<any>(loadPresets(workflow.uuid));
+ dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, DEFAULT_ADVANCED_FORM_VALUES));
}
};
const state = getState();
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;
+ const advancedForm = getFormValues(RUN_PROCESS_ADVANCED_FORM)(state) as RunProcessAdvancedFormData || DEFAULT_ADVANCED_FORM_VALUES;
const userUuid = getState().auth.user!.uuid;
const router = getState();
const properties = getState().properties;
mounts: createWorkflowMounts(selectedWorkflow, normalizeInputKeys(inputsForm)),
runtimeConstraints: {
API: true,
- vcpus: 1,
- ram: 1073741824,
+ vcpus: advancedForm[VCPUS_FIELD],
+ ram: advancedForm[RAM_FIELD],
+ api: advancedForm[API_FIELD],
+ },
+ schedulingParameters: {
+ maxRunTime: advancedForm[RUNTIME_FIELD]
},
containerImage: 'arvados/jobs',
cwd: '/var/spool/cwl',
command: [
'arvados-cwl-runner',
- '--local',
'--api=containers',
- `--project-uuid=${processOwnerUuid}`,
'/var/lib/cwl/workflow.json#main',
'/var/lib/cwl/cwl.input.json'
],
outputPath: '/var/spool/cwl',
priority: 1,
- outputName: advancedForm && advancedForm.output ? advancedForm.output : undefined,
+ outputName: advancedForm[OUTPUT_FIELD] ? advancedForm[OUTPUT_FIELD] : undefined,
};
const newProcess = await services.containerRequestService.create(newProcessData);
dispatch(navigateToProcess(newProcess.uuid));
}
};
+export const DEFAULT_ADVANCED_FORM_VALUES: Partial<RunProcessAdvancedFormData> = {
+ [VCPUS_FIELD]: 1,
+ [RAM_FIELD]: 1073741824,
+ [API_FIELD]: true,
+};
+
const normalizeInputKeys = (inputs: WorkflowInputsData): WorkflowInputsData =>
Object.keys(inputs).reduce((normalizedInputs, key) => ({
...normalizedInputs,
import { getAdvancedDataFromQuery, getQueryFromAdvancedData, parseSearchQuery } from "~/store/search-bar/search-bar-actions";
import { ResourceKind } from "~/models/resource";
-import { ClusterObjectType } from "~/models/search-bar";
describe('search-bar-actions', () => {
describe('parseSearchQuery', () => {
});
it('should correctly build advanced data record from query #2', () => {
- const r = getAdvancedDataFromQuery('document from:2017-08-01 pdf has:filesize:101mb is:trashed type:arvados#collection cluster:indianapolis');
+ const r = getAdvancedDataFromQuery('document from:2017-08-01 pdf has:filesize:101mb is:trashed type:arvados#collection cluster:c97qx');
expect(r).toEqual({
searchValue: 'document pdf',
type: ResourceKind.COLLECTION,
- cluster: ClusterObjectType.INDIANAPOLIS,
+ cluster: 'c97qx',
projectUuid: undefined,
inTrash: true,
dateFrom: '2017-08-01',
const q = getQueryFromAdvancedData({
searchValue: 'document pdf',
type: ResourceKind.COLLECTION,
- cluster: ClusterObjectType.INDIANAPOLIS,
+ cluster: 'c97qx',
projectUuid: undefined,
inTrash: true,
dateFrom: '2017-08-01',
saveQuery: false,
queryName: ''
});
- expect(q).toBe('document pdf type:arvados#collection cluster:indianapolis is:trashed from:2017-08-01 has:filesize:101mb');
+ expect(q).toBe('document pdf type:arvados#collection cluster:c97qx is:trashed from:2017-08-01 has:filesize:101mb');
});
});
});
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 { ClusterObjectType, PropertyValue, SearchBarAdvanceFormData } from '~/models/search-bar';
+import { PropertyValue, SearchBarAdvanceFormData } from '~/models/search-bar';
import { debounce } from 'debounce';
import * as _ from "lodash";
import { getModifiedKeysValues } from "~/common/objects";
import { activateSearchBarProject } from "~/store/search-bar/search-bar-tree-actions";
+import { Session } from "~/models/session";
+import { searchResultsPanelActions } from "~/store/search-results-panel/search-results-panel-actions";
+import { ListResults } from "~/services/common-service/common-service";
export const searchBarActions = unionize({
SET_CURRENT_VIEW: ofType<string>(),
dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
+ dispatch(searchResultsPanelActions.CLEAR());
dispatch(navigateToSearchResults);
};
const currentView = getState().searchBar.currentView;
if (searchValue || currentView === SearchView.ADVANCED) {
- const filters = getFilters('name', searchValue);
- const { items } = await services.groupsService.contents('', {
- filters,
- limit,
- recursive: true
- });
+ const sq = parseSearchQuery(searchValue);
+ const clusterId = getSearchQueryFirstProp(sq, 'cluster');
+ const sessions = getSearchSessions(clusterId, getState().auth.sessions);
+ const lists: ListResults<GroupContentsResource>[] = await Promise.all(sessions.map(session => {
+ const filters = getFilters('name', searchValue, sq);
+ return services.groupsService.contents('', {
+ filters,
+ limit,
+ recursive: true
+ }, session);
+ }));
+
+ const items = lists.reduce((items, list) => items.concat(list.items), [] as GroupContentsResource[]);
dispatch(searchBarActions.SET_SEARCH_RESULTS(items));
}
};
return value;
};
-export interface ParseSearchQuery {
+export class ParseSearchQuery {
hasKeywords: boolean;
values: string[];
properties: {
return { hasKeywords: keywordsCnt > 0, values, properties };
};
-const getFirstProp = (sq: ParseSearchQuery, name: string) => sq.properties[name] && sq.properties[name][0];
-const getPropValue = (sq: ParseSearchQuery, name: string, value: string) => sq.properties[name] && sq.properties[name].find((v: string) => v === value);
-const getProperties = (sq: ParseSearchQuery): PropertyValue[] => {
+export const getSearchQueryFirstProp = (sq: ParseSearchQuery, name: string) => sq.properties[name] && sq.properties[name][0];
+export const getSearchQueryPropValue = (sq: ParseSearchQuery, name: string, value: string) => sq.properties[name] && sq.properties[name].find((v: string) => v === value);
+export const getSearchQueryProperties = (sq: ParseSearchQuery): PropertyValue[] => {
if (sq.properties.has) {
return sq.properties.has.map((value: string) => {
const v = value.split(':');
return {
searchValue: sq.values.join(' '),
- type: getFirstProp(sq, 'type') as ResourceKind,
- cluster: getFirstProp(sq, 'cluster') as ClusterObjectType,
- projectUuid: getFirstProp(sq, 'project'),
- inTrash: getPropValue(sq, 'is', 'trashed') !== undefined,
- dateFrom: getFirstProp(sq, 'from'),
- dateTo: getFirstProp(sq, 'to'),
- properties: getProperties(sq),
+ type: getSearchQueryFirstProp(sq, 'type') as ResourceKind,
+ cluster: getSearchQueryFirstProp(sq, 'cluster'),
+ projectUuid: getSearchQueryFirstProp(sq, 'project'),
+ inTrash: getSearchQueryPropValue(sq, 'is', 'trashed') !== undefined,
+ dateFrom: getSearchQueryFirstProp(sq, 'from'),
+ dateTo: getSearchQueryFirstProp(sq, 'to'),
+ properties: getSearchQueryProperties(sq),
saveQuery: false,
queryName: ''
};
};
-export const getFilters = (filterName: string, searchValue: string): string => {
+export const getSearchSessions = (clusterId: string | undefined, sessions: Session[]): Session[] => {
+ return sessions.filter(s => s.loggedIn && (!clusterId || s.clusterId === clusterId));
+};
+
+export const getFilters = (filterName: string, searchValue: string, sq: ParseSearchQuery): string => {
const filter = new FilterBuilder();
- const sq = parseSearchQuery(searchValue);
- const resourceKind = getFirstProp(sq, 'type') as ResourceKind;
+ const resourceKind = getSearchQueryFirstProp(sq, 'type') as ResourceKind;
let prefix = '';
switch (resourceKind) {
break;
}
+ const isTrashed = getSearchQueryPropValue(sq, 'is', 'trashed');
+
if (!sq.hasKeywords) {
filter
.addILike(filterName, searchValue, GroupContentsResourcePrefix.COLLECTION)
- .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS)
.addILike(filterName, searchValue, GroupContentsResourcePrefix.PROJECT);
+
+ if (isTrashed) {
+ filter.addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS);
+ }
} else {
if (prefix) {
sq.values.forEach(v =>
sq.values.forEach(v => {
filter
.addILike(filterName, v, GroupContentsResourcePrefix.COLLECTION)
- .addILike(filterName, v, GroupContentsResourcePrefix.PROCESS)
.addILike(filterName, v, GroupContentsResourcePrefix.PROJECT);
+
+ if (isTrashed) {
+ filter.addILike(filterName, v, GroupContentsResourcePrefix.PROCESS);
+ }
});
}
- if (getPropValue(sq, 'is', 'trashed')) {
+ if (isTrashed) {
filter.addEqual("is_trashed", true);
}
- const projectUuid = getFirstProp(sq, 'project');
+ const projectUuid = getSearchQueryFirstProp(sq, 'project');
if (projectUuid) {
filter.addEqual('uuid', projectUuid, prefix);
}
- const dateFrom = getFirstProp(sq, 'from');
+ const dateFrom = getSearchQueryFirstProp(sq, 'from');
if (dateFrom) {
filter.addGte('modified_at', buildDateFilter(dateFrom));
}
- const dateTo = getFirstProp(sq, 'to');
+ const dateTo = getSearchQueryFirstProp(sq, 'to');
if (dateTo) {
filter.addLte('modified_at', buildDateFilter(dateTo));
}
- const props = getProperties(sq);
+ const props = getSearchQueryProperties(sq);
props.forEach(p => {
- // filter.addILike(`properties.${p.key}`, p.value);
+ if (p.value) {
+ filter.addILike(`properties.${p.key}`, p.value);
+ }
filter.addExists(p.key);
});
}
import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
import { ListResults } from '~/services/common-service/common-service';
import { searchResultsPanelActions } from '~/store/search-results-panel/search-results-panel-actions';
-import { getFilters } from '~/store/search-bar/search-bar-actions';
+import {
+ getFilters,
+ getSearchQueryFirstProp,
+ getSearchSessions, ParseSearchQuery,
+ parseSearchQuery
+} from '~/store/search-bar/search-bar-actions';
import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
export class SearchResultsMiddlewareService extends DataExplorerMiddlewareService {
super(id);
}
- async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean) {
const state = api.getState();
const userUuid = state.auth.user!.uuid;
const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
const searchValue = state.searchBar.searchValue;
+ const sq = parseSearchQuery(searchValue);
+ const clusterId = getSearchQueryFirstProp(sq, 'cluster');
+ const sessions = getSearchSessions(clusterId, state.auth.sessions);
+
+ if (searchValue.trim() === '') {
+ return;
+ }
+
try {
- const response = await this.services.groupsService.contents('', getParams(dataExplorer, searchValue));
- api.dispatch(updateResources(response.items));
- api.dispatch(setItems(response));
+ const params = getParams(dataExplorer, searchValue, sq);
+ const lists: ListResults<GroupContentsResource>[] = await Promise.all(sessions.map(session =>
+ this.services.groupsService.contents('', params, session)
+ ));
+
+ const items = lists
+ .reduce((items, list) => items.concat(list.items), [] as GroupContentsResource[]);
+
+ const itemsAvailable = lists
+ .reduce((itemsAvailable, list) => itemsAvailable + list.itemsAvailable, 0);
+
+ const list: ListResults<GroupContentsResource> = {
+ ...params,
+ kind: '',
+ items,
+ itemsAvailable
+ };
+
+ api.dispatch(updateResources(list.items));
+ api.dispatch(criteriaChanged
+ ? setItems(list)
+ : appendItems(list)
+ );
+
} catch {
- api.dispatch(couldNotFetchWorkflows());
+ api.dispatch(couldNotFetchSearchResults());
}
}
}
-export const getParams = (dataExplorer: DataExplorer, searchValue: string) => ({
+export const getParams = (dataExplorer: DataExplorer, searchValue: string, sq: ParseSearchQuery) => ({
...dataExplorerToListParams(dataExplorer),
- filters: getFilters('name', searchValue),
+ filters: getFilters('name', searchValue, sq),
order: getOrder(dataExplorer)
});
items: listResults.items.map(resource => resource.uuid),
});
-const couldNotFetchWorkflows = () =>
+export const appendItems = (listResults: ListResults<GroupContentsResource>) =>
+ searchResultsPanelActions.APPEND_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(listResults),
+ items: listResults.items.map(resource => resource.uuid),
+ });
+
+const couldNotFetchSearchResults = () =>
snackbarActions.OPEN_SNACKBAR({
- message: 'Could not fetch workflows.',
+ message: `Could not fetch search results for some sessions.`,
kind: SnackbarKind.ERROR
});
import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer';
import { UserMiddlewareService } from '~/store/users/user-panel-middleware-service';
import { USERS_PANEL_ID } from '~/store/users/users-actions';
-import { apiClientAuthorizationsReducer } from '~/store/api-client-authorizations/api-client-authorizations-reducer';
import { GroupsPanelMiddlewareService } from '~/store/groups-panel/groups-panel-middleware-service';
import { GROUPS_PANEL_ID } from '~/store/groups-panel/groups-panel-actions';
import { GroupDetailsPanelMiddlewareService } from '~/store/group-details-panel/group-details-panel-middleware-service';
import { LinkMiddlewareService } from '~/store/link-panel/link-panel-middleware-service';
import { COMPUTE_NODE_PANEL_ID } from '~/store/compute-nodes/compute-nodes-actions';
import { ComputeNodeMiddlewareService } from '~/store/compute-nodes/compute-nodes-middleware-service';
+import { API_CLIENT_AUTHORIZATION_PANEL_ID } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { ApiClientAuthorizationMiddlewareService } from '~/store/api-client-authorizations/api-client-authorizations-middleware-service';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
- window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
+ window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true, traceLimit: 25})) ||
compose;
export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
const groupDetailsPanelMiddleware = dataExplorerMiddleware(
new GroupDetailsPanelMiddlewareService(services, GROUP_DETAILS_PANEL_ID)
);
-
const linkPanelMiddleware = dataExplorerMiddleware(
new LinkMiddlewareService(services, LINK_PANEL_ID)
);
const computeNodeMiddleware = dataExplorerMiddleware(
new ComputeNodeMiddlewareService(services, COMPUTE_NODE_PANEL_ID)
);
+ const apiClientAuthorizationMiddlewareService = dataExplorerMiddleware(
+ new ApiClientAuthorizationMiddlewareService(services, API_CLIENT_AUTHORIZATION_PANEL_ID)
+ );
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
groupDetailsPanelMiddleware,
linkPanelMiddleware,
computeNodeMiddleware,
+ apiClientAuthorizationMiddlewareService
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
searchBar: searchBarReducer,
virtualMachines: virtualMachinesReducer,
repositories: repositoriesReducer,
- keepServices: keepServicesReducer,
- apiClientAuthorizations: apiClientAuthorizationsReducer
+ keepServices: keepServicesReducer
});
const otherFilters = new FilterBuilder()
.addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
+ // .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
.addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
.addEqual("is_trashed", true)
.getFilters();
import { UserResource } from "~/models/user";
import { getResource } from '~/store/resources/resources';
import { navigateToProject, navigateToUsers, navigateToRootProject } from "~/store/navigation/navigation-action";
+import { saveApiToken, getUserDetails } from '~/store/auth/auth-action';
export const USERS_PANEL_ID = 'usersPanel';
export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
export const USER_CREATE_FORM_NAME = 'userCreateFormName';
+export const USER_MANAGEMENT_DIALOG = 'userManageDialog';
+export const SETUP_SHELL_ACCOUNT_DIALOG = 'setupShellAccountDialog';
export interface UserCreateFormDialogData {
email: string;
- identityUrl: string;
virtualMachineName: string;
groupVirtualMachine: string;
}
dispatch(dialogActions.OPEN_DIALOG({ id: USER_ATTRIBUTES_DIALOG, data }));
};
+export const openUserManagement = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { resources } = getState();
+ const data = getResource<UserResource>(uuid)(resources);
+ dispatch(dialogActions.OPEN_DIALOG({ id: USER_MANAGEMENT_DIALOG, data }));
+ };
+
+export const openSetupShellAccount = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { resources } = getState();
+ const user = getResource<UserResource>(uuid)(resources);
+ const virtualMachines = await services.virtualMachineService.list();
+ dispatch(dialogActions.CLOSE_DIALOG({ id: USER_MANAGEMENT_DIALOG }));
+ dispatch(dialogActions.OPEN_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG, data: { user, ...virtualMachines } }));
+ };
+
+export const loginAs = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { resources } = getState();
+ const data = getResource<UserResource>(uuid)(resources);
+ if (data) {
+ services.authService.saveUser(data);
+ }
+ 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();
import { Dispatch } from 'redux';
import { RootState } from "~/store/store";
import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-import { loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action';
-import { openProjectPanel, projectPanelActions, setIsProjectPanelTrashed } from '~/store/project-panel/project-panel-action';
-import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { favoritePanelActions, loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action';
+import {
+ getProjectPanelCurrentUuid,
+ openProjectPanel,
+ projectPanelActions,
+ setIsProjectPanelTrashed
+} from '~/store/project-panel/project-panel-action';
+import {
+ activateSidePanelTreeItem,
+ initSidePanelTree,
+ loadSidePanelTreeProjects,
+ SidePanelTreeCategory
+} from '~/store/side-panel-tree/side-panel-tree-actions';
import { loadResource, updateResources } from '~/store/resources/resources-actions';
-import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
import { projectPanelColumns } from '~/views/project-panel/project-panel';
import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
import { matchRootRoute } from '~/routes/routes';
-import { setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs, setTrashBreadcrumbs, setBreadcrumbs, setGroupDetailsBreadcrumbs, setGroupsBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+import {
+ setBreadcrumbs,
+ setGroupDetailsBreadcrumbs,
+ setGroupsBreadcrumbs,
+ setProcessBreadcrumbs,
+ setSharedWithMeBreadcrumbs,
+ setSidePanelBreadcrumbs,
+ setTrashBreadcrumbs
+} from '~/store/breadcrumbs/breadcrumbs-actions';
import { navigateToProject } from '~/store/navigation/navigation-action';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { ServiceRepository } from '~/services/services';
import { getResource } from '~/store/resources/resources';
-import { getProjectPanelCurrentUuid } from '~/store/project-panel/project-panel-action';
import * as projectCreateActions from '~/store/projects/project-create-actions';
import * as projectMoveActions from '~/store/projects/project-move-actions';
import * as projectUpdateActions from '~/store/projects/project-update-actions';
import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
import { initProcessLogsPanel } from '~/store/process-logs-panel/process-logs-panel-actions';
import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
-import { sharedWithMePanelActions } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
-import { loadSharedWithMePanel } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
+import {
+ loadSharedWithMePanel,
+ sharedWithMePanelActions
+} from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
import { loadSshKeysPanel } from '~/store/auth/auth-action-ssh';
import { workflowPanelColumns } from '~/views/workflow-panel/workflow-panel-view';
import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
import { getProgressIndicator } from '~/store/progress-indicator/progress-indicator-reducer';
-import { ResourceKind, extractUuidKind } from '~/models/resource';
+import { extractUuidKind, ResourceKind } from '~/models/resource';
import { FilterBuilder } from '~/services/api/filter-builder';
import { GroupContentsResource } from '~/services/groups-service/groups-service';
-import { unionize, ofType, UnionOf, MatchCases } from '~/common/unionize';
+import { MatchCases, ofType, unionize, UnionOf } from '~/common/unionize';
import { loadRunProcessPanel } from '~/store/run-process-panel/run-process-panel-actions';
import { loadCollectionFiles } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
-import { SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
import { CollectionResource } from "~/models/collection";
-import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/search-results-panel/search-results-panel-actions';
+import {
+ loadSearchResultsPanel,
+ searchResultsPanelActions
+} from '~/store/search-results-panel/search-results-panel-actions';
import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view';
import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machines-actions';
import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions';
import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions';
import { loadUsersPanel, userBindedActions } from '~/store/users/users-actions';
-import { loadLinkPanel, linkPanelActions } from '~/store/link-panel/link-panel-actions';
-import { loadComputeNodesPanel, computeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
+import { linkPanelActions, loadLinkPanel } from '~/store/link-panel/link-panel-actions';
+import { computeNodesActions, loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions';
import { linkPanelColumns } from '~/views/link-panel/link-panel-root';
import { userPanelColumns } from '~/views/user-panel/user-panel';
import { computeNodePanelColumns } from '~/views/compute-node-panel/compute-node-panel-root';
-import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+import { apiClientAuthorizationPanelColumns } from '~/views/api-client-authorization-panel/api-client-authorization-panel-root';
import * as groupPanelActions from '~/store/groups-panel/groups-panel-actions';
import { groupsPanelColumns } from '~/views/groups-panel/groups-panel';
import * as groupDetailsPanelActions from '~/store/group-details-panel/group-details-panel-actions';
import { groupDetailsPanelColumns } from '~/views/group-details-panel/group-details-panel';
+import { DataTableFetchMode } from "~/components/data-table/data-table";
export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
+ dispatch(searchResultsPanelActions.SET_FETCH_MODE({ fetchMode: DataTableFetchMode.INFINITE }));
dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
dispatch(groupDetailsPanelActions.GroupDetailsPanelActions.SET_COLUMNS({columns: groupDetailsPanelColumns}));
dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
+ dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
+
dispatch<any>(initSidePanelTree());
if (router.location) {
const match = matchRootRoute(router.location.pathname);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { isNumber } from 'lodash';
+
+export const ERROR_MESSAGE = (minValue: number) => `Minimum value is ${minValue}`;
+
+export const min =
+ (minValue: number, errorMessage = ERROR_MESSAGE) =>
+ (value: any) =>
+ isNumber(value) && value >= minValue ? undefined : errorMessage(minValue);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const optional = (validator: (value: any) => string | undefined) =>
+ (value: any) =>
+ value === undefined || value === null || value === '' ? undefined : validator(value);
\ No newline at end of file
{value === 4 && dialogContent(curlHeader, curlExample, classes)}
</DialogContent>
<DialogActions>
- <Button variant='flat' color='primary' onClick={closeDialog}>
+ <Button variant='text' color='primary' onClick={closeDialog}>
Close
</Button>
</DialogActions>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
import { openContextMenu, openCollectionFilesContextMenu } from '~/store/context-menu/context-menu-actions';
import { openUploadCollectionFilesDialog } from '~/store/collections/collection-upload-actions';
import { ResourceKind } from "~/models/resource";
+import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
const memoizedMapStateToProps = () => {
let prevState: CollectionPanelFilesState;
};
};
-const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
+const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onFileClick' | 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
onUploadDataClick: () => {
dispatch<any>(openUploadCollectionFilesDialog());
},
onOptionsMenuOpen: (event) => {
dispatch<any>(openCollectionFilesContextMenu(event));
},
+ onFileClick: (id) => {
+ dispatch(openDetailsPanel(id));
+ },
});
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
import { AdvancedIcon, ProjectIcon, AttributesIcon, UserPanelIcon } from "~/components/icon/icon";
import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
-import { openUserAttributes, openUserProjects } from "~/store/users/users-actions";
+import { openUserAttributes, openUserProjects, openUserManagement } from "~/store/users/users-actions";
export const userActionSet: ContextMenuActionSet = [[{
name: "Attributes",
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
}
-},
-{
+}, {
name: "Manage",
icon: UserPanelIcon,
execute: (dispatch, { uuid }) => {
- dispatch<any>(openAdvancedTabDialog(uuid));
+ dispatch<any>(openUserManagement(uuid));
}
}]];
maxWidth='md'>
<DialogTitle>Current Token</DialogTitle>
<DialogContent>
- <Typography variant='body1' paragraph={true}>
+ <Typography paragraph={true}>
The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
<Typography component='p'>
For more information see
</a>
</Typography>
</Typography>
- <Typography variant='body1' paragraph={true}>
+ <Typography paragraph={true}>
Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
</Typography>
<DefaultCodeSnippet lines={[getSnippet(data)]} />
- <Typography variant='body1'>
+ <Typography >
Arvados
<a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
},
+ onLoadMore: (page: number) => {
+ dispatch(dataExplorerActions.SET_PAGE({ id, page }));
+ },
+
onRowClick,
onRowDoubleClick,
</Grid>
</Grid>;
-export const RosurceWorkflowName = connect(
+export const ResourceWorkflowName = connect(
(state: RootState, props: { uuid: string }) => {
const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
return resource || { name: '', uuid: '', kind: '', ownerUuid: '' };
return resource || { username: '' };
})(renderUsername);
-// Compute Node Resources
-const renderNodeDate = (date: string) =>
+// Common methods
+const renderCommonData = (data: string) =>
+ <Typography noWrap>{data}</Typography>;
+
+const renderCommonDate = (date: string) =>
<Typography noWrap>{formatDate(date)}</Typography>;
-const renderNodeData = (data: string) => {
- return <Typography noWrap>{data}</Typography>;
-};
+export const CommonUuid = withResourceData('uuid', renderCommonData);
+
+// Api Client Authorizations
+export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
+
+export const TokenApiToken = withResourceData('apiToken', renderCommonData);
+
+export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
+
+export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
+
+export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
+
+export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
+export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
+
+export const TokenScopes = withResourceData('scopes', renderCommonData);
+
+export const TokenUserId = withResourceData('userId', renderCommonData);
+
+// Compute Node Resources
const renderNodeInfo = (data: string) => {
return <Typography>{JSON.stringify(data, null, 4)}</Typography>;
};
-export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
+const clusterColors = [
+ ['#f44336', '#fff'],
+ ['#2196f3', '#fff'],
+ ['#009688', '#fff'],
+ ['#cddc39', '#fff'],
+ ['#ff9800', '#fff']
+];
+
+export const ResourceCluster = (props: { uuid: string }) => {
+ const CLUSTER_ID_LENGTH = 5;
+ const pos = props.uuid.indexOf('-');
+ 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>
+ <div style={{
+ backgroundColor: clusterColors[ci][0],
+ color: clusterColors[ci][1],
+ padding: "2px 7px",
+ borderRadius: 3
+ }}>{clusterId}</div>
+ </Typography>;
+};
-export const ComputeNodeUuid = withResourceData('uuid', renderNodeData);
+export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
-export const ComputeNodeDomain = withResourceData('domain', renderNodeData);
+export const ComputeNodeDomain = withResourceData('domain', renderCommonData);
-export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderNodeDate);
+export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderCommonDate);
-export const ComputeNodeHostname = withResourceData('hostname', renderNodeData);
+export const ComputeNodeHostname = withResourceData('hostname', renderCommonData);
-export const ComputeNodeIpAddress = withResourceData('ipAddress', renderNodeData);
+export const ComputeNodeIpAddress = withResourceData('ipAddress', renderCommonData);
-export const ComputeNodeJobUuid = withResourceData('jobUuid', renderNodeData);
+export const ComputeNodeJobUuid = withResourceData('jobUuid', renderCommonData);
-export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderNodeDate);
+export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderCommonDate);
// Links Resources
const renderLinkName = (item: { name: string }) =>
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';
type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
},
});
-const getItem = (resource: DetailsResource, resourceData?: ResourceData): DetailsData => {
- const res = resource || { kind: undefined, name: 'Projects' };
- switch (res.kind) {
- case ResourceKind.PROJECT:
- return new ProjectDetails(res);
- case ResourceKind.COLLECTION:
- return new CollectionDetails(res, resourceData);
- case ResourceKind.PROCESS:
- return new ProcessDetails(res);
- default:
- return new EmptyDetails(res as EmptyResource);
+const EMPTY_RESOURCE: EmptyResource = { kind: undefined, name: 'Projects' };
+
+const getItem = (res: DetailsResource, resourceData?: ResourceData): DetailsData => {
+ if ('kind' in res) {
+ switch (res.kind) {
+ case ResourceKind.PROJECT:
+ return new ProjectDetails(res);
+ case ResourceKind.COLLECTION:
+ return new CollectionDetails(res, resourceData);
+ case ResourceKind.PROCESS:
+ return new ProcessDetails(res);
+ default:
+ return new EmptyDetails(res);
+ }
+ } else {
+ return new FileDetails(res);
}
};
-const mapStateToProps = ({ detailsPanel, resources, resourcesData }: RootState) => {
- const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource;
+const mapStateToProps = ({ detailsPanel, resources, resourcesData, 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, resourceData)
+ item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData)
};
};
</Grid>
<Grid item xs={8}>
<Tooltip title={item.getTitle()}>
- <Typography variant="title" noWrap>
+ <Typography variant='h6' noWrap>
{item.getTitle()}
</Typography>
</Tooltip>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DetailsData } from "./details-data";
+import { CollectionFile, CollectionDirectory, CollectionFileType } from '~/models/collection-file';
+import { getIcon } from '~/components/file-tree/file-tree-item';
+import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
+import { formatFileSize } from '~/common/formatters';
+import { FileThumbnail } from '~/components/file-tree/file-thumbnail';
+import isImage from 'is-image';
+
+export class FileDetails extends DetailsData<CollectionFile | CollectionDirectory> {
+
+ getIcon(className?: string) {
+ const Icon = getIcon(this.item.type);
+ return <Icon className={className} />;
+ }
+
+ getDetails() {
+ const { item } = this;
+ return item.type === CollectionFileType.FILE
+ ? <>
+ <DetailsAttribute label='Size' value={formatFileSize(item.size)} />
+ {
+ isImage(item.url) && <>
+ <DetailsAttribute label='Preview' />
+ <FileThumbnail file={item} />
+ </>
+ }
+ </>
+ : <div />;
+ }
+}
import { InjectedFormProps } from 'redux-form';
import { WithDialogProps } from '~/store/dialog/with-dialog';
import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { UserEmailField, UserIdentityUrlField, UserVirtualMachineField, UserGroupsVirtualMachineField } from '~/views-components/form-fields/user-form-fields';
+import { UserEmailField, UserVirtualMachineField, UserGroupsVirtualMachineField } from '~/views-components/form-fields/user-form-fields';
export type DialogUserProps = WithDialogProps<{}> & InjectedFormProps<any>;
const UserAddFields = (props: DialogUserProps) => <span>
<UserEmailField />
- <UserIdentityUrlField />
<UserVirtualMachineField data={props.data}/>
<UserGroupsVirtualMachineField />
</span>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import * as React from 'react';
+import { compose } from "redux";
+import { reduxForm, InjectedFormProps, Field } from 'redux-form';
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { TextField } from '~/components/text-field/text-field';
+import { VirtualMachinesResource } from '~/models/virtual-machines';
+import { USER_LENGTH_VALIDATION } from '~/validators/validators';
+import { InputLabel } from '@material-ui/core';
+import { NativeSelectField } from '~/components/select-field/select-field';
+import { SETUP_SHELL_ACCOUNT_DIALOG, createUser } from '~/store/users/users-actions';
+import { UserResource } from '~/models/user';
+
+interface SetupShellAccountFormDialogData {
+ email: string;
+ virtualMachineName: string;
+ groupVirtualMachine: string;
+}
+
+export const SetupShellAccountDialog = compose(
+ withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
+ reduxForm<SetupShellAccountFormDialogData>({
+ form: SETUP_SHELL_ACCOUNT_DIALOG,
+ onSubmit: (data, dispatch) => {
+ dispatch(createUser(data));
+ }
+ })
+)(
+ (props: SetupShellAccountDialogComponentProps) =>
+ <FormDialog
+ dialogTitle='Setup shell account'
+ formFields={SetupShellAccountFormFields}
+ submitLabel='Submit'
+ {...props}
+ />
+);
+
+interface UserProps {
+ data: {
+ user: UserResource;
+ };
+}
+
+interface VirtualMachinesProps {
+ data: {
+ items: VirtualMachinesResource[];
+ };
+}
+interface DataProps {
+ user: UserResource;
+ items: VirtualMachinesResource[];
+}
+
+const UserEmailField = ({ data }: UserProps) =>
+ <span>
+ <Field
+ name='email'
+ component={TextField}
+ disabled
+ label={data.user.email} /></span>;
+
+const UserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
+ <div style={{ marginBottom: '21px' }}>
+ <InputLabel>Virtual Machine</InputLabel>
+ <Field
+ name='virtualMachine'
+ component={NativeSelectField}
+ validate={USER_LENGTH_VALIDATION}
+ items={getVirtualMachinesList(data.items)} />
+ </div>;
+
+const UserGroupsVirtualMachineField = () =>
+ <Field
+ name='groups'
+ component={TextField}
+ validate={USER_LENGTH_VALIDATION}
+ label="Groups for virtual machine (comma separated list)" />;
+
+const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
+ virtualMachines.map(it => ({ key: it.hostname, value: it.hostname }));
+
+type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<SetupShellAccountFormDialogData>;
+
+const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProps) =>
+ <>
+ <UserEmailField data={props.data as DataProps} />
+ <UserVirtualMachineField data={props.data as DataProps} />
+ <UserGroupsVirtualMachineField />
+ </>;
+
+
+
import { CheckboxField } from '~/components/checkbox-field/checkbox-field';
import { NativeSelectField } from '~/components/select-field/select-field';
import { ResourceKind } from '~/models/resource';
-import { ClusterObjectType } from '~/models/search-bar';
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 { SearchBarAdvancedPropertiesView } from '~/views-components/search-bar/search-bar-advanced-properties-view';
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 { connect } from "react-redux";
+import { RootState } from "~/store/store";
export const SearchBarTypeField = () =>
<Field
{ key: ResourceKind.PROCESS, value: 'Process' }
]} />;
-export const SearchBarClusterField = () =>
- <Field
+
+interface SearchBarClusterFieldProps {
+ clusters: { key: string, value: string }[];
+}
+
+export const SearchBarClusterField = connect(
+ (state: RootState) => ({
+ clusters: [{key: '', value: 'Any'}].concat(
+ state.auth.sessions
+ .filter(s => s.loggedIn)
+ .map(s => ({
+ key: s.clusterId,
+ value: s.clusterId
+ })))
+ }))((props: SearchBarClusterFieldProps) => <Field
name='cluster'
component={NativeSelectField}
- items={[
- { key: '', value: 'Any' },
- { key: ClusterObjectType.INDIANAPOLIS, value: 'Indianapolis' },
- { key: ClusterObjectType.KAISERAUGST, value: 'Kaiseraugst' },
- { key: ClusterObjectType.PENZBERG, value: 'Penzberg' }
- ]} />;
+ items={props.clusters}/>
+ );
export const SearchBarProjectField = () =>
<Field
autoFocus={true}
label="Email" />;
-export const UserIdentityUrlField = () =>
- <Field
- name='identityUrl'
- component={TextField}
- validate={USER_LENGTH_VALIDATION}
- label="Identity URL Prefix" />;
-
export const UserVirtualMachineField = ({ data }: any) =>
<div style={{ marginBottom: '21px' }}>
<InputLabel>Virtual Machine</InputLabel>
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data && attributes(props.data, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data && attributes(props.data, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
<MenuItem key={link.title}>
<a href={link.link} target="_blank" className={classes.link}>
<ImportContactsIcon className={classes.icon} />
- <Typography variant="body1" className={classes.linkTitle}>{link.title}</Typography>
+ <Typography className={classes.linkTitle}>{link.title}</Typography>
</a>
</MenuItem>
)
<Toolbar className={props.classes.toolbar}>
<Grid container justify="space-between">
<Grid container item xs={3} direction="column" justify="center">
- <Typography variant="title" color="inherit" noWrap>
+ <Typography variant='h6' color="inherit" noWrap>
<Link to={Routes.ROOT} className={props.classes.link}>
arvados workbench
</Link>
lines={[props.data.command]} />
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Cancel
</Button>
- <Button variant='raised' color='primary'>
+ <Button variant='contained' color='primary'>
Remove
</Button>
</DialogActions>
<DefaultCodeSnippet
className={props.classes.codeSnippet}
lines={[snippetText(props.data.uuidPrefix)]} />
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
See also:
<div><a href="https://doc.arvados.org/user/getting_started/ssh-access-unix.html" className={props.classes.link} target="_blank">SSH access</a></div>
<div><a href="https://doc.arvados.org/user/tutorials/tutorial-firstscript.html" className={props.classes.link} target="_blank">Writing a Crunch Script</a></div>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data.repositoryData && attributes(props.data.repositoryData, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
{props.advancedViewOpen &&
<>
<Grid item>
- <Typography variant='subheading'>
+ <Typography variant='subtitle1'>
Who can access
</Typography>
<SharingPublicAccessForm />
<Divider />
<Grid container alignItems='center' spacing={8} wrap='nowrap' className={classes.root}>
<Grid item xs={8}>
- <Typography noWrap variant='subheading'>{fields.get(index).email}</Typography>
+ <Typography noWrap variant='subtitle1'>{fields.get(index).email}</Typography>
</Grid>
<Grid item xs={4} container wrap='nowrap'>
<Field
<Divider />
<Grid container alignItems='center' spacing={8} className={classes.root}>
<Grid item xs={8}>
- <Typography variant='subheading'>
+ <Typography variant='subtitle1'>
{renderVisibilityInfo(visibility)}
</Typography>
</Grid>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data && attributes(props.data, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from "@material-ui/core";
+import { WithDialogProps } from "~/store/dialog/with-dialog";
+import { withDialog } from '~/store/dialog/with-dialog';
+import { WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { compose, Dispatch } from "redux";
+import { USER_MANAGEMENT_DIALOG, openSetupShellAccount, loginAs } from "~/store/users/users-actions";
+import { connect } from "react-redux";
+
+type CssRules = 'spacing';
+
+const styles = withStyles<CssRules>((theme: ArvadosTheme) => ({
+ spacing: {
+ paddingBottom: theme.spacing.unit * 2,
+ paddingTop: theme.spacing.unit * 2,
+ }
+}));
+
+interface UserManageDataProps {
+ data: any;
+}
+
+interface UserManageActionProps {
+ openSetupShellAccount: (uuid: string) => void;
+ loginAs: (uuid: string) => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ openSetupShellAccount: (uuid: string) => dispatch<any>(openSetupShellAccount(uuid)),
+ loginAs: (uuid: string) => dispatch<any>(loginAs(uuid))
+});
+
+type UserManageProps = UserManageDataProps & UserManageActionProps & WithStyles<CssRules>;
+
+export const UserManageDialog = compose(
+ connect(null, mapDispatchToProps),
+ withDialog(USER_MANAGEMENT_DIALOG),
+ styles)(
+ (props: WithDialogProps<UserManageProps> & UserManageProps) =>
+ <Dialog open={props.open}
+ onClose={props.closeDialog}
+ fullWidth
+ maxWidth="md">
+ {props.data &&
+ <span>
+ <DialogTitle>{`Manage - ${props.data.firstName} ${props.data.lastName}`}</DialogTitle>
+ <DialogContent>
+ <Typography variant='body1' className={props.classes.spacing}>
+ As an admin, you can log in as this user. When you’ve finished, you will need to log out and log in again with your own account.
+ </Typography>
+ <Button variant="contained" color="primary" onClick={() => props.loginAs(props.data.uuid)}>
+ {`LOG IN AS ${props.data.firstName} ${props.data.lastName}`}
+ </Button>
+ <Typography variant='body1' className={props.classes.spacing}>
+ As an admin, you can setup a shell account for this user. The login name is automatically generated from the user's e-mail address.
+ </Typography>
+ <Button variant="contained" color="primary" onClick={() => props.openSetupShellAccount(props.data.uuid)}>
+ {`SETUP SHELL ACCOUNT FOR ${props.data.firstName} ${props.data.lastName}`}
+ </Button>
+ </DialogContent></span>}
+
+ <DialogActions>
+ <Button
+ variant='text'
+ color='primary'
+ onClick={props.closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+ );
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data.virtualMachineData && attributes(props.data.virtualMachineData, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import {
- StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid,
- Table, TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton
+import {
+ StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Tooltip, IconButton
} from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
-import { MoreOptionsIcon, HelpIcon } from '~/components/icon/icon';
-import { ApiClientAuthorization } from '~/models/api-client-authorization';
-import { formatDate } from '~/common/formatters';
+import { HelpIcon, ShareMeIcon } from '~/components/icon/icon';
+import { createTree } from '~/models/tree';
+import { DataColumns } from '~/components/data-table/data-table';
+import { SortDirection } from '~/components/data-table/data-column';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { API_CLIENT_AUTHORIZATION_PANEL_ID } from '../../store/api-client-authorizations/api-client-authorizations-actions';
+import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
+import { ResourcesState } from '~/store/resources/resources';
+import {
+ CommonUuid, TokenApiClientId, TokenApiToken, TokenCreatedByIpAddress, TokenDefaultOwnerUuid, TokenExpiresAt,
+ TokenLastUsedAt, TokenLastUsedByIpAddress, TokenScopes, TokenUserId
+} from '~/views-components/data-explorer/renderers';
-type CssRules = 'root' | 'tableRow' | 'helpIconGrid' | 'tableGrid';
+type CssRules = 'card' | 'cardContent' | 'helpIconGrid';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- root: {
+ card: {
width: '100%',
overflow: 'auto'
},
+ cardContent: {
+ padding: 0,
+ '&:last-child': {
+ paddingBottom: 0
+ }
+ },
helpIconGrid: {
textAlign: 'right'
+ }
+});
+
+
+export enum ApiClientAuthorizationPanelColumnNames {
+ UUID = 'UUID',
+ API_CLIENT_ID = 'API Client ID',
+ API_TOKEN = 'API Token',
+ CREATED_BY_IP_ADDRESS = 'Created by IP address',
+ DEFAULT_OWNER_UUID = 'Default owner',
+ EXPIRES_AT = 'Expires at',
+ LAST_USED_AT = 'Last used at',
+ LAST_USED_BY_IP_ADDRESS = 'Last used by IP address',
+ SCOPES = 'Scopes',
+ USER_ID = 'User ID'
+}
+
+export const apiClientAuthorizationPanelColumns: DataColumns<string> = [
+ {
+ name: ApiClientAuthorizationPanelColumnNames.UUID,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: createTree(),
+ render: uuid => <CommonUuid uuid={uuid} />
},
- tableGrid: {
- marginTop: theme.spacing.unit
+ {
+ name: ApiClientAuthorizationPanelColumnNames.API_CLIENT_ID,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <TokenApiClientId uuid={uuid} />
},
- tableRow: {
- '& td, th': {
- whiteSpace: 'nowrap'
- }
+ {
+ name: ApiClientAuthorizationPanelColumnNames.API_TOKEN,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <TokenApiToken uuid={uuid} />
+ },
+ {
+ name: ApiClientAuthorizationPanelColumnNames.CREATED_BY_IP_ADDRESS,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <TokenCreatedByIpAddress uuid={uuid} />
+ },
+ {
+ name: ApiClientAuthorizationPanelColumnNames.DEFAULT_OWNER_UUID,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <TokenDefaultOwnerUuid uuid={uuid} />
+ },
+ {
+ name: ApiClientAuthorizationPanelColumnNames.EXPIRES_AT,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <TokenExpiresAt uuid={uuid} />
+ },
+ {
+ name: ApiClientAuthorizationPanelColumnNames.LAST_USED_AT,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <TokenLastUsedAt uuid={uuid} />
+ },
+ {
+ name: ApiClientAuthorizationPanelColumnNames.LAST_USED_BY_IP_ADDRESS,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <TokenLastUsedByIpAddress uuid={uuid} />
+ },
+ {
+ name: ApiClientAuthorizationPanelColumnNames.SCOPES,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <TokenScopes uuid={uuid} />
+ },
+ {
+ name: ApiClientAuthorizationPanelColumnNames.USER_ID,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <TokenUserId uuid={uuid} />
}
-});
+];
+
+const DEFAULT_MESSAGE = 'Your api client authorization list is empty.';
export interface ApiClientAuthorizationPanelRootActionProps {
- openRowOptions: (event: React.MouseEvent<HTMLElement>, keepService: ApiClientAuthorization) => void;
+ onItemClick: (item: string) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
+ onItemDoubleClick: (item: string) => void;
openHelpDialog: () => void;
}
export interface ApiClientAuthorizationPanelRootDataProps {
- apiClientAuthorizations: ApiClientAuthorization[];
- hasApiClientAuthorizations: boolean;
+ resources: ResourcesState;
}
-type ApiClientAuthorizationPanelRootProps = ApiClientAuthorizationPanelRootActionProps
+type ApiClientAuthorizationPanelRootProps = ApiClientAuthorizationPanelRootActionProps
& ApiClientAuthorizationPanelRootDataProps & WithStyles<CssRules>;
export const ApiClientAuthorizationPanelRoot = withStyles(styles)(
- ({ classes, hasApiClientAuthorizations, apiClientAuthorizations, openRowOptions, openHelpDialog }: ApiClientAuthorizationPanelRootProps) =>
- <Card className={classes.root}>
- <CardContent>
- {hasApiClientAuthorizations && <Grid container direction="row" justify="flex-end">
+ ({ classes, onItemDoubleClick, onItemClick, onContextMenu, openHelpDialog }: ApiClientAuthorizationPanelRootProps) =>
+ <Card className={classes.card}>
+ <CardContent className={classes.cardContent}>
+ <Grid container direction="row" justify="flex-end">
<Grid item xs={12} className={classes.helpIconGrid}>
<Tooltip title="Api token - help">
<IconButton onClick={openHelpDialog}>
</Tooltip>
</Grid>
<Grid item xs={12}>
- <Table>
- <TableHead>
- <TableRow className={classes.tableRow}>
- <TableCell>UUID</TableCell>
- <TableCell>API Client ID</TableCell>
- <TableCell>API Token</TableCell>
- <TableCell>Created by IP address</TableCell>
- <TableCell>Default owner</TableCell>
- <TableCell>Expires at</TableCell>
- <TableCell>Last used at</TableCell>
- <TableCell>Last used by IP address</TableCell>
- <TableCell>Scopes</TableCell>
- <TableCell>User ID</TableCell>
- <TableCell />
- </TableRow>
- </TableHead>
- <TableBody>
- {apiClientAuthorizations.map((apiClientAuthorizatio, index) =>
- <TableRow key={index} className={classes.tableRow}>
- <TableCell>{apiClientAuthorizatio.uuid}</TableCell>
- <TableCell>{apiClientAuthorizatio.apiClientId}</TableCell>
- <TableCell>{apiClientAuthorizatio.apiToken}</TableCell>
- <TableCell>{apiClientAuthorizatio.createdByIpAddress || '(none)'}</TableCell>
- <TableCell>{apiClientAuthorizatio.defaultOwnerUuid || '(none)'}</TableCell>
- <TableCell>{formatDate(apiClientAuthorizatio.expiresAt) || '(none)'}</TableCell>
- <TableCell>{formatDate(apiClientAuthorizatio.lastUsedAt) || '(none)'}</TableCell>
- <TableCell>{apiClientAuthorizatio.lastUsedByIpAddress || '(none)'}</TableCell>
- <TableCell>{JSON.stringify(apiClientAuthorizatio.scopes)}</TableCell>
- <TableCell>{apiClientAuthorizatio.userId}</TableCell>
- <TableCell>
- <Tooltip title="More options" disableFocusListener>
- <IconButton onClick={event => openRowOptions(event, apiClientAuthorizatio)}>
- <MoreOptionsIcon />
- </IconButton>
- </Tooltip>
- </TableCell>
- </TableRow>)}
- </TableBody>
- </Table>
+ <DataExplorer
+ id={API_CLIENT_AUTHORIZATION_PANEL_ID}
+ onRowClick={onItemClick}
+ onRowDoubleClick={onItemDoubleClick}
+ onContextMenu={onContextMenu}
+ contextMenuColumn={true}
+ hideColumnSelector
+ hideSearchInput
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={ShareMeIcon}
+ messages={[DEFAULT_MESSAGE]} />
+ } />
</Grid>
- </Grid>}
+ </Grid>
</CardContent>
</Card>
);
\ No newline at end of file
const mapStateToProps = (state: RootState): ApiClientAuthorizationPanelRootDataProps => {
return {
- apiClientAuthorizations: state.apiClientAuthorizations,
- hasApiClientAuthorizations: state.apiClientAuthorizations.length > 0
+ resources: state.resources
};
};
const mapDispatchToProps = (dispatch: Dispatch): ApiClientAuthorizationPanelRootActionProps => ({
- openRowOptions: (event, apiClientAuthorization) => {
+ onContextMenu: (event, apiClientAuthorization) => {
dispatch<any>(openApiClientAuthorizationContextMenu(event, apiClientAuthorization));
},
+ onItemClick: (resourceUuid: string) => { return; },
+ onItemDoubleClick: uuid => { return; },
openHelpDialog: () => {
dispatch<any>(openApiClientAuthorizationsHelpDialog());
}
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';
type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value' | 'link';
return { item, data };
})(
class extends React.Component<CollectionPanelProps> {
+
render() {
const { classes, item, data, dispatch } = this.props;
return item
? <>
<Card className={classes.card}>
<CardHeader
- avatar={<CollectionIcon className={classes.iconHeader} />}
+ avatar={
+ <IconButton onClick={this.openCollectionDetails}>
+ <CollectionIcon className={classes.iconHeader} />
+ </IconButton>
+ }
action={
<Tooltip title="More options" disableFocusListener>
<IconButton
</Tooltip>
}
title={item && item.name}
- subheader={item && item.description} />
+ titleTypographyProps={this.titleProps}
+ subheader={item && item.description}
+ subheaderTypographyProps={this.titleProps} />
<CardContent>
<Grid container direction="column">
<Grid item xs={6}>
label='Content size' value={data && formatFileSize(data.fileSize)} />
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
label='Owner' value={item && item.ownerUuid} />
- <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
+ <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
<DetailsAttribute classLabel={classes.link} label='Link to process' />
</span>
</Grid>
kind: SnackbarKind.SUCCESS
}));
}
+
+ openCollectionDetails = () => {
+ const { item } = this.props;
+ if (item) {
+ this.props.dispatch(openDetailsPanel(item.uuid));
+ }
+ }
+
+ titleProps = {
+ onClick: this.openCollectionDetails
+ };
+
}
)
);
import { DataColumns } from '~/components/data-table/data-table';
import { SortDirection } from '~/components/data-table/data-column';
import { createTree } from '~/models/tree';
-import {
- ComputeNodeUuid, ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
- ComputeNodeFirstPingAt, ComputeNodeLastPingAt, ComputeNodeIpAddress
+import {
+ ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
+ ComputeNodeFirstPingAt, ComputeNodeLastPingAt, ComputeNodeIpAddress, CommonUuid
} from '~/views-components/data-explorer/renderers';
import { ResourcesState } from '~/store/resources/resources';
configurable: true,
sortDirection: SortDirection.NONE,
filters: createTree(),
- render: uuid => <ComputeNodeUuid uuid={uuid} />
+ render: uuid => <CommonUuid uuid={uuid} />
},
{
name: ComputeNodePanelColumnNames.DOMAIN,
icon={ShareMeIcon}
messages={[DEFAULT_MESSAGE]} />
} />;
-};
\ No newline at end of file
+};
({ classes, dispatch }: LoginPanelProps) =>
<Grid container direction="column" item xs alignItems="center" justify="center" className={classes.root}>
<Grid item className={classes.container}>
- <Typography variant="title" align="center" className={classes.title}>
+ <Typography variant='h6' align="center" className={classes.title}>
Welcome to the Arvados Workbench
</Typography>
- <Typography variant="body1" className={classes.content}>
+ <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 variant="body1" className={classes.content}>
+ <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="body2" className={classNames(classes.content, classes.content__bolder)}>
+ <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 variant="body1" className={classes.content}>
+ <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 component="div" align="right">
({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting }: MyAccountPanelRootProps) => {
return <Card className={classes.root}>
<CardContent>
- <Typography variant="title" className={classes.title}>User profile</Typography>
+ <Typography variant='h6' className={classes.title}>User profile</Typography>
<form onSubmit={handleSubmit}>
<Grid container direction="row" spacing={24}>
<Grid item xs={6}>
}
},
typography: {
- fontFamily: 'monospace'
+ fontFamily: 'monospace',
+ useNextVariants: true,
}
});
</Tooltip>}
title={
<Tooltip title={process.containerRequest.name} placement="bottom-start">
- <Typography noWrap variant="title" className={classes.title}>
+ <Typography noWrap variant='h6' className={classes.title}>
{process.containerRequest.name}
</Typography>
</Tooltip>}
}
title={
<Tooltip title={process.containerRequest.name} placement="bottom-start">
- <Typography noWrap variant="title" color='inherit'>
+ <Typography noWrap variant='h6' color='inherit'>
{process.containerRequest.name}
</Typography>
</Tooltip>
}
subheader={
<Tooltip title={getDescription(process)} placement="bottom-start">
- <Typography noWrap variant="body2" color='inherit'>
+ <Typography noWrap variant='body1' color='inherit'>
{getDescription(process)}
</Typography>
</Tooltip>}/>
classes={{ content: classes.title, action: classes.action }}
action={
<div className={classes.rightSideHeader}>
- <Typography noWrap variant="body2" className={classes.status}>
+ <Typography noWrap variant='body1' className={classes.status}>
{getProcessStatus(subprocess)}
</Typography>
<Tooltip title="More options" disableFocusListener>
className={classes.options}
aria-label="More options"
onClick={onContextMenu}>
- <MoreOptionsIcon className={classes.moreOptions}/>
+ <MoreOptionsIcon className={classes.moreOptions} />
</IconButton>
</Tooltip>
</div>
}
title={
<Tooltip title={subprocess.containerRequest.name}>
- <Typography noWrap variant="body2" className={classes.titleHeader}>
+ <Typography noWrap variant='body1' className={classes.titleHeader}>
{subprocess.containerRequest.name}
</Typography>
</Tooltip>
} />
<CardContent className={classes.content}>
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label="Runtime" value={formatTime(getProcessRuntime(subprocess))} />
+ label="Runtime" value={subprocess.container && subprocess.container.startedAt && subprocess.container.finishedAt
+ ? formatTime(getProcessRuntime(subprocess)) :
+ '(none)'} />
</CardContent>
</Card>;
});
<CardHeader
className={classes.title}
title={
- <Typography noWrap variant="title" color='inherit'>
+ <Typography noWrap variant='h6' color='inherit'>
Subprocess and filters
</Typography>} />
<CardContent>
<CardContent>
<Grid container direction="row">
<Grid item xs={8}>
- <Typography variant="body2">
+ <Typography variant='body1'>
When you are using an Arvados virtual machine, you should clone the https:// URLs. This will authenticate automatically using your API token. <br />
In order to clone git repositories using SSH, <Link to={Routes.SSH_KEYS_USER} className={classes.link}>add an SSH key to your account</Link> and clone the git@ URLs.
</Typography>
</div>
<Divider />
<div className={classes.chips}>
- <Typography variant='subheading'>Selected collections ({this.state.directories.length}):</Typography>
+ <Typography variant='subtitle1'>Selected collections ({this.state.directories.length}):</Typography>
<Chips
orderable
deletable
</div>
<Divider />
<div className={classes.chips}>
- <Typography variant='subheading'>Selected files ({this.state.files.length}):</Typography>
+ <Typography variant='subtitle1'>Selected files ({this.state.files.length}):</Typography>
<Chips
orderable
deletable
format={format}
validate={getValidation(input)} />;
-const parse = (value: any) => parseInt(value, 10);
+export const parse = (value: any) => value === '' ? '' : parseInt(value, 10);
-const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value);
+export const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value);
const getValidation = memoize(
(input: IntCommandInputParameter) => ([
import { Grid } from '@material-ui/core';
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 RUN_PROCESS_ADVANCED_FORM = 'runProcessAdvancedForm';
+export const OUTPUT_FIELD = 'output';
+export const RUNTIME_FIELD = 'runtime';
+export const RAM_FIELD = 'ram';
+export const VCPUS_FIELD = 'vcpus';
+export const KEEP_CACHE_RAM_FIELD = 'keepCacheRam';
+export const API_FIELD = 'api';
+
export interface RunProcessAdvancedFormData {
- output: string;
- runtime: string;
+ [OUTPUT_FIELD]?: string;
+ [RUNTIME_FIELD]?: number;
+ [RAM_FIELD]: number;
+ [VCPUS_FIELD]: number;
+ [KEEP_CACHE_RAM_FIELD]?: number;
+ [API_FIELD]?: boolean;
}
export const RunProcessAdvancedForm =
reduxForm<RunProcessAdvancedFormData>({
- form: RUN_PROCESS_ADVANCED_FORM
+ form: RUN_PROCESS_ADVANCED_FORM,
})(() =>
<form>
<ExpansionPanel elevation={0}>
<Grid container spacing={32}>
<Grid item xs={12} md={6}>
<Field
- name='output'
+ name={OUTPUT_FIELD}
component={TextField}
label="Output name" />
</Grid>
<Grid item xs={12} md={6}>
<Field
- name='runtime'
+ name={RUNTIME_FIELD}
+ component={TextField}
+ helperText="Maximum running time (in seconds) that this container will be allowed to run before being cancelled."
+ label="Runtime limit"
+ parse={IntInput.parse}
+ format={IntInput.format}
+ type='number'
+ validate={runtimeValidation} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <Field
+ name={RAM_FIELD}
+ component={TextField}
+ label="RAM"
+ helperText="Number of ram bytes to be used to run this process."
+ parse={IntInput.parse}
+ format={IntInput.format}
+ type='number'
+ required
+ validate={ramValidation} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <Field
+ name={VCPUS_FIELD}
component={TextField}
- label="Runtime limit (hh)" />
+ label="VCPUs"
+ helperText="Number of cores to be used to run this process."
+ parse={IntInput.parse}
+ format={IntInput.format}
+ type='number'
+ required
+ validate={vcpusValidation} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <Field
+ name={KEEP_CACHE_RAM_FIELD}
+ component={TextField}
+ label="Keep cache RAM"
+ helperText="Number of keep cache bytes to be used to run this process."
+ parse={IntInput.parse}
+ format={IntInput.format}
+ type='number'
+ validate={keepCacheRamValdation} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <Field
+ name={API_FIELD}
+ component={SwitchField}
+ switchProps={{
+ color: 'primary'
+ }}
+ label='API'
+ helperText='When set, ARVADOS_API_HOST and ARVADOS_API_TOKEN will be set, and process will have networking enabled to access the Arvados API server.' />
</Grid>
</Grid>
</ExpansionPanelDetails>
</ExpansionPanel>
</form >);
+
+const ramValidation = [min(0)];
+const vcpusValidation = [min(1)];
+const keepCacheRamValdation = [optional(min(0))];
+const runtimeValidation = [optional(min(1))];
import { RootState } from '~/store/store';
import { isValid } from 'redux-form';
import { RUN_PROCESS_INPUTS_FORM } from './run-process-inputs-form';
-import { RunProcessAdvancedForm } from './run-process-advanced-form';
+import { RunProcessAdvancedForm, RUN_PROCESS_ADVANCED_FORM } from './run-process-advanced-form';
import { createSelector, createStructuredSelector } from 'reselect';
import { WorkflowPresetSelect } from '~/views/run-process-panel/workflow-preset-select';
import { selectPreset } from '~/store/run-process-panel/run-process-panel-actions';
state.runProcessPanel.inputs;
const validSelector = (state: RootState) =>
- isValid(RUN_PROCESS_BASIC_FORM)(state) && isValid(RUN_PROCESS_INPUTS_FORM)(state);
+ isValid(RUN_PROCESS_BASIC_FORM)(state) && isValid(RUN_PROCESS_INPUTS_FORM)(state) && isValid(RUN_PROCESS_ADVANCED_FORM)(state);
const mapStateToProps = createStructuredSelector({
inputs: inputsSelector,
import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
import {
- ProcessStatus,
+ ProcessStatus, ResourceCluster,
ResourceFileSize,
ResourceLastModifiedDate,
ResourceName,
import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
export enum SearchResultsPanelColumnNames {
+ CLUSTER = "Cluster",
NAME = "Name",
PROJECT = "Project",
STATUS = "Status",
}
export const searchResultsPanelColumns: DataColumns<string> = [
+ {
+ name: SearchResultsPanelColumnNames.CLUSTER,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: (uuid: string) => <ResourceCluster uuid={uuid} />
+ },
{
name: SearchResultsPanelColumnNames.NAME,
selected: true,
onRowDoubleClick={props.onItemDoubleClick}
onContextMenu={props.onContextMenu}
contextMenuColumn={true} />;
-};
\ No newline at end of file
+};
<CardContent>
<Grid container direction="row">
<Grid item xs={12}>
- <Typography variant='body1' 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>
</Grid>
<form onSubmit={handleSubmit}>
<Grid container direction="row">
<Grid item xs={12}>
- <Typography variant='body1' 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>
</Grid>
<CardContent>
<Grid container direction="row">
<Grid item xs={8}>
- { !hasKeys && <Typography variant='body1' paragraph={true} >
+ { !hasKeys && <Typography paragraph={true} >
You have not yet set up an SSH public key for use with Arvados.
<a href='https://doc.arvados.org/user/getting_started/ssh-access-unix.html'
target='blank' className={classes.link}>
Learn more.
</a>
</Typography>}
- { !hasKeys && <Typography variant='body1' paragraph={true}>
+ { !hasKeys && <Typography paragraph={true}>
When you have an SSH key you would like to use, add it using button below.
</Typography> }
</Grid>
import { Dispatch } from "redux";
import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
import { createTree } from '~/models/tree';
-import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
+import {
+ getInitialResourceTypeFilters,
+ getTrashPanelTypeFilters
+} from '~/store/resource-type-filters/resource-type-filters';
type CssRules = "toolbar" | "button";
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
- filters: getInitialResourceTypeFilters(),
+ filters: getTrashPanelTypeFilters(),
render: uuid => <ResourceType uuid={uuid} />,
},
{
import { UserResource } from '~/models/user';
import { ShareMeIcon, AddIcon } from '~/components/icon/icon';
import { USERS_PANEL_ID, openUserCreateDialog } from '~/store/users/users-actions';
+import { noop } from 'lodash';
type UserPanelRules = "button";
<span>
<DataExplorer
id={USERS_PANEL_ID}
- onRowClick={this.handleRowClick}
- onRowDoubleClick={this.handleRowDoubleClick}
+ onRowClick={noop}
+ onRowDoubleClick={noop}
onContextMenu={this.handleContextMenu}
contextMenuColumn={true}
hideColumnSelector
});
}
}
-
- handleRowDoubleClick = (uuid: string) => {
- this.props.handleRowDoubleClick(uuid);
- }
-
- handleRowClick = () => {
- return;
- }
}
);
<Card>
<CardContent className={props.classes.cardWithoutMachines}>
<Grid item xs={6}>
- <Typography variant="body2">
+ <Typography variant='body1'>
You do not have access to any virtual machines. Some Arvados features require using the command line. You may request access to a hosted virtual machine with the command line shell.
</Typography>
</Grid>
SEND REQUEST FOR SHELL ACCESS
</Button>
{props.requestedDate &&
- <Typography variant="body1">
+ <Typography >
A request for shell access was sent on {props.requestedDate}
</Typography>}
</span>;
<Grid item xs={12}>
<Card>
<CardContent>
- <Typography variant="body2">
+ <Typography variant='body1'>
In order to access virtual machines using SSH, <Link to={Routes.SSH_KEYS_USER} className={props.classes.link}>add an SSH key to your account</Link> and add a section like this to your SSH configuration file ( ~/.ssh/config):
</Typography>
<DefaultCodeSnippet
import { UserAttributesDialog } from '~/views-components/user-dialog/attributes-dialog';
import { CreateUserDialog } from '~/views-components/dialog-forms/create-user-dialog';
import { HelpApiClientAuthorizationDialog } from '~/views-components/api-client-authorizations-dialog/help-dialog';
+import { UserManageDialog } from '~/views-components/user-dialog/manage-dialog';
+import { SetupShellAccountDialog } from '~/views-components/dialog-forms/setup-shell-account-dialog';
import { GroupsPanel } from '~/views/groups-panel/groups-panel';
import { CreateGroupDialog } from '~/views-components/dialog-forms/create-group-dialog';
import { RemoveGroupDialog } from '~/views-components/groups-dialog/remove-dialog';
<RepositoryAttributesDialog />
<RepositoriesSampleGitDialog />
<RichTextEditorDialog />
+ <SetupShellAccountDialog />
<SharingDialog />
<Snackbar />
<UpdateCollectionDialog />
<UpdateProcessDialog />
<UpdateProjectDialog />
<UserAttributesDialog />
+ <UserManageDialog />
<VirtualMachineAttributesDialog />
</Grid>
);
render() {
const { classes, workflow } = this.props;
- if (workflow) {
- console.log(workflow.definition);
- }
const { value } = this.state;
return <div className={classes.root}>
<Tabs value={value} onChange={this.handleChange} centered={true}>
import { WORKFLOW_PANEL_ID } from '~/store/workflow-panel/workflow-panel-actions';
import {
ResourceLastModifiedDate,
- RosurceWorkflowName,
+ ResourceWorkflowName,
ResourceWorkflowStatus,
ResourceShare,
ResourceRunProcess
configurable: true,
sortDirection: SortDirection.ASC,
filters: createTree(),
- render: (uuid: string) => <RosurceWorkflowName uuid={uuid} />
+ render: (uuid: string) => <ResourceWorkflowName uuid={uuid} />
},
{
name: WorkflowPanelColumnNames.AUTHORISATION,