cy.get('main').contains(projectName).rightclick();
- cy.get('[data-cy=context-menu]').contains('Advanced').click();
+ cy.get('[data-cy=context-menu]').contains('API Details').click();
cy.get('[role=tablist]').contains('METADATA').click();
});
});
});
+
+ describe('Frozen projects', () => {
+ beforeEach(() => {
+ cy.createGroup(activeUser.token, {
+ name: `Main project ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ }).as('mainProject');
+
+ cy.createGroup(adminUser.token, {
+ name: `Admin project ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ }).as('adminProject').then((mainProject) => {
+ cy.shareWith(adminUser.token, activeUser.user.uuid, mainProject.uuid, 'can_write');
+ });
+
+ cy.get('@mainProject').then((mainProject) => {
+ cy.createGroup(adminUser.token, {
+ name : `Sub project ${Math.floor(Math.random() * 999999)}`,
+ group_class: 'project',
+ owner_uuid: mainProject.uuid,
+ }).as('subProject');
+
+ cy.createCollection(adminUser.token, {
+ name: `Main collection ${Math.floor(Math.random() * 999999)}`,
+ owner_uuid: mainProject.uuid,
+ manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ }).as('mainCollection');
+ });
+ });
+
+ it('should be able to froze own project', () => {
+ cy.getAll('@mainProject').then(([mainProject]) => {
+ cy.loginAs(activeUser);
+
+ cy.get('[data-cy=project-panel]').contains(mainProject.name).rightclick();
+
+ cy.get('[data-cy=context-menu]').contains('Freeze').click();
+
+ cy.get('[data-cy=project-panel]').contains(mainProject.name).rightclick();
+
+ cy.get('[data-cy=context-menu]').contains('Freeze').should('not.exist');
+ });
+ });
+
+ it('should not be able to modify items within the frozen project', () => {
+ cy.getAll('@mainProject', '@mainCollection').then(([mainProject, mainCollection]) => {
+ cy.loginAs(activeUser);
+
+ cy.get('[data-cy=project-panel]').contains(mainProject.name).rightclick();
+
+ cy.get('[data-cy=context-menu]').contains('Freeze').click();
+
+ cy.get('[data-cy=project-panel]').contains(mainProject.name).click();
+
+ cy.get('[data-cy=project-panel]').contains(mainCollection.name).rightclick();
+
+ cy.get('[data-cy=context-menu]').contains('Move to trash').should('not.exist');
+ });
+ });
+
+ it('should be able to froze not owned project', () => {
+ cy.getAll('@adminProject').then(([adminProject]) => {
+ cy.loginAs(activeUser);
+
+ cy.get('[data-cy=side-panel-tree]').contains('Shared with me').click();
+
+ cy.get('main').contains(adminProject.name).rightclick();
+
+ cy.get('[data-cy=context-menu]').contains('Freeze').should('not.exist');
+ });
+ });
+
+ it('should be able to unfroze project if user is an admin', () => {
+ cy.getAll('@adminProject').then(([adminProject]) => {
+ cy.loginAs(adminUser);
+
+ cy.get('main').contains(adminProject.name).rightclick();
+
+ cy.get('[data-cy=context-menu]').contains('Freeze').click();
+
+ cy.wait(1000);
+
+ cy.get('main').contains(adminProject.name).rightclick();
+
+ cy.get('[data-cy=context-menu]').contains('Unfreeze').click();
+
+ cy.get('main').contains(adminProject.name).rightclick();
+
+ cy.get('[data-cy=context-menu]').contains('Freeze').should('exist');
+ });
+ });
+ });
+
+ it('copies project URL to clipboard', () => {
+ const projectName = `Test project (${Math.floor(999999 * Math.random())})`;
+
+ cy.loginAs(activeUser);
+ cy.get('[data-cy=side-panel-button]').click();
+ cy.get('[data-cy=side-panel-new-project]').click();
+ cy.get('[data-cy=form-dialog]')
+ .should('contain', 'New Project')
+ .within(() => {
+ cy.get('[data-cy=name-field]').within(() => {
+ cy.get('input').type(projectName);
+ });
+ cy.get('[data-cy=form-submit-btn]').click();
+ });
+
+ cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+ cy.get('[data-cy=project-panel]').contains(projectName).rightclick();
+ cy.get('[data-cy=context-menu]').contains('Copy to clipboard').click();
+ cy.window().then((win) => (
+ win.navigator.clipboard.readText().then((text) => {
+ expect(text).to.match(/https\:\/\/localhost\:[0-9]+\/projects\/[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/,);
+ })
+ ));
+
+ });
});
+
});
});
+ it('can search items using quotes', function() {
+ const random = Math.floor(Math.random() * Math.floor(999999));
+ const colName = `Collection ${random}`;
+ const colName2 = `Collection test ${random}`;
+
+ // Creates the collection using the admin token so we can set up
+ // a bogus manifest text without block signatures.
+ cy.createCollection(adminUser.token, {
+ name: colName,
+ owner_uuid: activeUser.user.uuid,
+ preserve_version: true,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ }).as('collection1');
+
+ cy.createCollection(adminUser.token, {
+ name: colName2,
+ owner_uuid: activeUser.user.uuid,
+ preserve_version: true,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ }).as('collection2');
+
+ cy.getAll('@collection1', '@collection2')
+ .then(function() {
+ cy.loginAs(activeUser);
+
+ cy.doSearch(colName);
+ cy.get('[data-cy=search-results] table tbody tr').should('have.length', 2);
+
+ cy.doSearch(`"${colName}"`);
+ cy.get('[data-cy=search-results] table tbody tr').should('have.length', 1);
+ });
+ });
+
it('can display owner of the item', function() {
const colName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
cy.get('[data-cy=context-menu]').within((ctx) => {
// Check that there are 4 items in the menu
cy.get(ctx).children().should('have.length', 4);
- cy.contains('Advanced');
+ cy.contains('API Details');
cy.contains('Copy to clipboard');
cy.contains('Open in new tab');
cy.contains('View details');
}) {
cy.get('[data-cy=user-profile-panel-options-btn]').click();
cy.get('[data-cy=context-menu]').within(() => {
- cy.get('[role=button]').contains('Advanced');
+ cy.get('[role=button]').contains('API Details');
cy.get('[role=button]').should(account ? 'contain' : 'not.contain', 'Account Settings');
cy.get('[role=button]').should(activate ? 'contain' : 'not.contain', 'Activate User');
})
});
cy.get('[role=tooltip]').click();
- cy.get('[data-cy=form-dialog]')
+ cy.get('[data-cy=form-dialog]').as('add-login-dialog')
.should('contain', 'Add login permission')
.within(() => {
cy.get('label')
.contains('Add groups')
.parent()
.within(() => {
- cy.get('input').type('docker sudo{enter}');
+ cy.get('input').type('docker ');
+ // Veryfy submit enabled (form has changed)
+ cy.get('@add-login-dialog').within(() => {
+ cy.get('[data-cy=form-submit-btn]').should('be.enabled');
+ });
+ cy.get('input').type('sudo');
+ // Veryfy submit disabled (partial input in chips)
+ cy.get('@add-login-dialog').within(() => {
+ cy.get('[data-cy=form-submit-btn]').should('be.disabled');
+ });
+ cy.get('input').type('{enter}');
})
});
cy.get('[data-cy=form-dialog]').within(() => {
cy.get('[data-cy=form-submit-btn]').click();
});
- cy.get('[data-cy=snackbar]').contains('Permission updated');
+
cy.get('[data-cy=vm-admin-table]')
.contains(vmHost)
.parents('tr')
cy.get('[data-cy=form-dialog]').within(() => {
cy.get('[data-cy=form-submit-btn]').click();
});
- cy.get('[data-cy=snackbar]').contains('Permission updated');
+
cy.get('[data-cy=vm-admin-table]')
.contains(vmHost)
.parents('tr')
});
// Wait for page to finish loading
- cy.get('[data-cy=snackbar]').contains('Permission updated');
cy.get('[data-cy=vm-admin-table]')
.contains(vmHost)
.parents('tr')
cy.get('[data-cy=form-dialog]').within(() => {
cy.get('[data-cy=form-submit-btn]').click();
});
- cy.get('[data-cy=snackbar]').contains('Permission updated');
// Verify new login permissions
// Check admin's vm page for login
}
export interface ClusterConfigJSON {
+ API: {
+ UnfreezeProjectRequiresAdmin: boolean
+ },
ClusterID: string;
RemoteClusters: {
[key: string]: {
};
export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): ClusterConfigJSON => ({
+ API: {
+ UnfreezeProjectRequiresAdmin: false,
+ },
ClusterID: "",
RemoteClusters: {},
Services: {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ProjectResource } from "models/project";
+import { getResource } from "store/resources/resources";
+
+export const resourceIsFrozen = (resource: any, resources): boolean => {
+ let isFrozen: boolean = !!resource.frozenByUuid;
+ let ownerUuid: string | undefined = resource?.ownerUuid;
+
+ while(!isFrozen && !!ownerUuid && ownerUuid.indexOf('000000000000000') === -1) {
+ const parentResource: ProjectResource | undefined = getResource<ProjectResource>(ownerUuid)(resources);
+ isFrozen = !!parentResource?.frozenByUuid;
+ ownerUuid = parentResource?.ownerUuid;
+ }
+
+ return isFrozen;
+}
\ No newline at end of file
private static instance: ServicesProvider;
+ private store;
private services;
private constructor() {}
}
return this.services;
}
+
+ public setStore(newStore): void {
+ if (!this.store) {
+ this.store = newStore;
+ }
+ }
+
+ public getStore() {
+ if (!this.store) {
+ throw "Please check if store has been set in the index.ts before the app is initiated"; // eslint-disable-line no-throw-literal
+ }
+
+ return this.store;
+ }
}
export default ServicesProvider.getInstance();
describe("<Breadcrumbs />", () => {
let onClick: () => void;
+ let resources = {};
beforeEach(() => {
onClick = jest.fn();
const items = [
{ label: 'breadcrumb 1' }
];
- const breadcrumbs = shallow(<Breadcrumbs items={items} onClick={onClick} onContextMenu={jest.fn()} />).dive();
+ const breadcrumbs = shallow(<Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />).dive();
expect(breadcrumbs.find(Button)).toHaveLength(1);
expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(0);
});
{ label: 'breadcrumb 1' },
{ label: 'breadcrumb 2' }
];
- const breadcrumbs = shallow(<Breadcrumbs items={items} onClick={onClick} onContextMenu={jest.fn()} />).dive();
+ const breadcrumbs = shallow(<Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />).dive();
expect(breadcrumbs.find(Button)).toHaveLength(2);
expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(1);
});
{ label: 'breadcrumb 1' },
{ label: 'breadcrumb 2' }
];
- const breadcrumbs = shallow(<Breadcrumbs items={items} onClick={onClick} onContextMenu={jest.fn()} />).dive();
+ const breadcrumbs = shallow(<Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />).dive();
breadcrumbs.find(Button).at(1).simulate('click');
expect(onClick).toBeCalledWith(items[1]);
});
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import { withStyles } from '@material-ui/core';
import { IllegalNamingWarning } from '../warning/warning';
-import { IconType } from 'components/icon/icon';
+import { IconType, FreezeIcon } from 'components/icon/icon';
import grey from '@material-ui/core/colors/grey';
+import { ResourceBreadcrumb } from 'store/breadcrumbs/breadcrumbs-actions';
+import { ResourcesState } from 'store/resources/resources';
export interface Breadcrumb {
label: string;
icon?: IconType;
}
-type CssRules = "item" | "currentItem" | "label" | "icon";
+type CssRules = "item" | "currentItem" | "label" | "icon" | "frozenIcon";
const styles: StyleRulesCallback<CssRules> = theme => ({
item: {
},
icon: {
fontSize: 20,
- color: grey["600"]
+ color: grey["600"],
+ marginRight: '10px',
+ },
+ frozenIcon: {
+ fontSize: 20,
+ color: grey["600"],
+ marginLeft: '10px',
},
});
export interface BreadcrumbsProps {
- items: Breadcrumb[];
- onClick: (breadcrumb: Breadcrumb) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
+ items: ResourceBreadcrumb[];
+ resources: ResourcesState;
+ onClick: (breadcrumb: ResourceBreadcrumb) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: ResourceBreadcrumb) => void;
}
export const Breadcrumbs = withStyles(styles)(
- ({ classes, onClick, onContextMenu, items }: BreadcrumbsProps & WithStyles<CssRules>) =>
+ ({ classes, onClick, onContextMenu, items, resources }: BreadcrumbsProps & WithStyles<CssRules>) =>
<Grid container data-cy='breadcrumbs' alignItems="center" wrap="nowrap">
{
items.map((item, index) => {
className={classes.label}>
{item.label}
</Typography>
+ {
+ (resources[item.uuid] as any)?.frozenByUuid ? <FreezeIcon className={classes.frozenIcon} /> : null
+ }
</Button>
</Tooltip>
{!isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />}
values: Value[];
getLabel?: (value: Value) => string;
onChange: (value: Value[]) => void;
+ onPartialInput?: (value: boolean) => void;
handleFocus?: (e: any) => void;
handleBlur?: (e: any) => void;
chipsClassName?: string;
setText = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ text: event.target.value }, () => {
+ // Update partial input status
+ this.props.onPartialInput && this.props.onPartialInput(this.state.text !== '');
+
// If pattern is provided, check for delimiter
if (this.props.pattern) {
const matches = this.state.text.match(this.props.pattern);
this.setState({ text: '' });
this.props.onChange([...this.props.values, newValue]);
}
+ this.props.onPartialInput && this.props.onPartialInput(false);
}
}
className?: string;
apiResponse?: boolean;
linked?: boolean;
+ children?: JSX.Element;
}
interface CodeSnippetAuthProps {
});
export const CodeSnippet = withStyles(styles)(connect(mapStateToProps)(
- ({ classes, lines, linked, className, apiResponse, dispatch, auth }: CodeSnippetProps & CodeSnippetAuthProps & DispatchProp) =>
+ ({ classes, lines, linked, className, apiResponse, dispatch, auth, children }: CodeSnippetProps & CodeSnippetAuthProps & DispatchProp) =>
<Typography
component="div"
className={classNames(classes.root, className)}>
<Typography className={apiResponse ? classes.space : className} component="pre">
+ {children}
{linked ?
lines.map((line, index) => <React.Fragment key={index}>{renderLinks(auth, dispatch)(line)}{`\n`}</React.Fragment>) :
lines.join('\n')
const currentPDH = (collectionPanel.item || {}).portableDataHash;
React.useEffect(() => {
if (currentPDH) {
- // Avoid fetching the same content level twice
- if (leftKey !== rightKey) {
- fetchData([leftKey, rightKey], true);
- } else {
- fetchData(rightKey, true);
- }
+ fetchData([leftKey, rightKey], true);
}
}, [currentPDH]); // eslint-disable-line react-hooks/exhaustive-deps
: <div className={classes.rowEmpty}>No directories available</div>
}}
</AutoSizer>
- : <div className={classes.row}><CircularProgress className={classes.loader} size={30} /></div> }
+ : <div data-cy="collection-loader" className={classes.row}><CircularProgress className={classes.loader} size={30} /></div> }
</div>
</div>
<div className={classes.rightPanel} data-cy="collection-files-right-panel">
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
import Warning from '@material-ui/icons/Warning';
-import Visibility from '@material-ui/icons/Visibility';
-import VisibilityOff from '@material-ui/icons/VisibilityOff';
import VpnKey from '@material-ui/icons/VpnKey';
import LinkOutlined from '@material-ui/icons/LinkOutlined';
import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
faEllipsisH,
);
+export const FreezeIcon = (props: any) =>
+ <span {...props}>
+ <span className='fas fa-snowflake' />
+ </span>
+
+export const UnfreezeIcon = (props: any) =>
+ <div {...props}>
+ <div className="fa-layers fa-1x fa-fw">
+ <span className="fas fa-slash"
+ data-fa-mask="fas fa-snowflake" data-fa-transform="down-1.5" />
+ <span className="fas fa-slash" />
+ </div>
+ </div>;
+
export const PendingIcon = (props: any) =>
<span {...props}>
<span className='fas fa-ellipsis-h' />
export const TrashIcon: IconType = (props) => <Delete {...props} />;
export const UserPanelIcon: IconType = (props) => <Person {...props} />;
export const UsedByIcon: IconType = (props) => <Folder {...props} />;
-export const VisibleIcon: IconType = (props) => <Visibility {...props} />;
-export const InvisibleIcon: IconType = (props) => <VisibilityOff {...props} />;
export const WorkflowIcon: IconType = (props) => <Code {...props} />;
export const WarningIcon: IconType = (props) => <Warning style={{ color: '#fbc02d', height: '30px', width: '30px' }} {...props} />;
export const Link: IconType = (props) => <LinkOutlined {...props} />;
import { GridProps } from '@material-ui/core/Grid';
import { isArray } from 'lodash';
import { DefaultView } from 'components/default-view/default-view';
-import { InfoIcon, InvisibleIcon, VisibleIcon } from 'components/icon/icon';
+import { InfoIcon } from 'components/icon/icon';
import { ReactNodeArray } from 'prop-types';
import classNames from 'classnames';
(panelStates[idx] &&
(panelStates[idx].visible || panelStates[idx].visible === undefined)));
const [panelVisibility, setPanelVisibility] = useState<boolean[]>(visibility);
- const [brightenedPanel, setBrightenedPanel] = useState<number>(-1);
+ const [highlightedPanel, setHighlightedPanel] = useState<number>(-1);
+ const [selectedPanel, setSelectedPanel] = useState<number>(-1);
const panelRef = useRef<any>(null);
let panels: JSX.Element[] = [];
- let toggles: JSX.Element[] = [];
+ let buttons: JSX.Element[] = [];
if (isArray(children)) {
for (let idx = 0; idx < children.length; idx++) {
true,
...panelVisibility.slice(idx+1)
]);
+ setSelectedPanel(idx);
};
const hideFn = (idx: number) => () => {
setPanelVisibility([
...panelVisibility.slice(idx+1).map(() => false),
])
};
- const toggleIcon = panelVisibility[idx]
- ? <VisibleIcon className={classNames(classes.buttonIcon)} />
- : <InvisibleIcon className={classNames(classes.buttonIcon)}/>
const panelName = panelStates === undefined
? `Panel ${idx+1}`
: (panelStates[idx] && panelStates[idx].name) || `Panel ${idx+1}`;
- const toggleVariant = "outlined";
- const toggleTooltip = panelVisibility[idx]
- ? ''
- :`Show ${panelName} panel`;
+ const btnVariant = panelVisibility[idx]
+ ? "contained"
+ : "outlined";
+ const btnTooltip = panelVisibility[idx]
+ ? ``
+ :`Open ${panelName} panel`;
const panelIsMaximized = panelVisibility[idx] &&
panelVisibility.filter(e => e).length === 1;
- let brightenerTimer: NodeJS.Timer;
- toggles = [
- ...toggles,
- <Tooltip title={toggleTooltip} disableFocusListener>
- <Button variant={toggleVariant} size="small" color="primary"
+ buttons = [
+ ...buttons,
+ <Tooltip title={btnTooltip} disableFocusListener>
+ <Button variant={btnVariant} size="small" color="primary"
className={classNames(classes.button)}
onMouseEnter={() => {
- brightenerTimer = setTimeout(
- () => setBrightenedPanel(idx), 100);
+ setHighlightedPanel(idx);
}}
onMouseLeave={() => {
- brightenerTimer && clearTimeout(brightenerTimer);
- setBrightenedPanel(-1);
+ setHighlightedPanel(-1);
}}
onClick={showFn(idx)}>
{panelName}
- {toggleIcon}
</Button>
</Tooltip>
];
const aPanel =
<MPVHideablePanel key={idx} visible={panelVisibility[idx]} name={panelName}
- panelRef={(idx === brightenedPanel) ? panelRef : undefined}
- maximized={panelIsMaximized} illuminated={idx === brightenedPanel}
+ panelRef={(idx === selectedPanel) ? panelRef : undefined}
+ maximized={panelIsMaximized} illuminated={idx === highlightedPanel}
doHidePanel={hideFn(idx)} doMaximizePanel={maximizeFn(idx)}>
{children[idx]}
</MPVHideablePanel>;
return <Grid container {...props}>
<Grid container item direction="row">
- { toggles.map((tgl, idx) => <Grid item key={idx}>{tgl}</Grid>) }
+ { buttons.map((tgl, idx) => <Grid item key={idx}>{tgl}</Grid>) }
</Grid>
- <Grid container item {...props} xs className={classes.content}>
+ <Grid container item {...props} xs className={classes.content}
+ onScroll={() => setSelectedPanel(-1)}>
{ panelVisibility.includes(true)
? panels
: <Grid container item alignItems='center' justify='center'>
describe("on input target change", () => {
it("clears the input value on selfClearProp change", () => {
const searchInput = mount(<SearchInput selfClearProp="abc" value="123" onSearch={onSearch} debounce={1000}/>);
- searchInput.setProps({ selfClearProp: 'aaa' });
+
+ // component should clear value upon creation
jest.runTimersToTime(1000);
expect(onSearch).toBeCalledWith("");
expect(onSearch).toHaveBeenCalledTimes(1);
+
+ // component should not clear on same selfClearProp
+ searchInput.setProps({ selfClearProp: 'abc' });
+ jest.runTimersToTime(1000);
+ expect(onSearch).toHaveBeenCalledTimes(1);
+
+ // component should clear on selfClearProp change
+ searchInput.setProps({ selfClearProp: '111' });
+ jest.runTimersToTime(1000);
+ expect(onSearch).toBeCalledWith("");
+ expect(onSearch).toHaveBeenCalledTimes(2);
});
});
-
});
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
+import React, {useState, useEffect} from 'react';
import { IconButton, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment, Tooltip } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyles<CssRules>;
-interface SearchInputState {
- value: string;
- label: string;
- selfClearProp: string;
-}
-
export const DEFAULT_SEARCH_DEBOUNCE = 1000;
-export const SearchInput = withStyles(styles)(
- class extends React.Component<SearchInputProps> {
- state: SearchInputState = {
- value: "",
- label: "",
- selfClearProp: ""
- };
+const SearchInputComponent = (props: SearchInputProps) => {
+ const [timeout, setTimeout] = useState(0);
+ const [value, setValue] = useState("");
+ const [label, setLabel] = useState("Search");
+ const [selfClearProp, setSelfClearProp] = useState("");
- timeout: number;
-
- render() {
- return <form onSubmit={this.handleSubmit}>
- <FormControl>
- <InputLabel>{this.state.label}</InputLabel>
- <Input
- type="text"
- data-cy="search-input"
- value={this.state.value}
- onChange={this.handleChange}
- endAdornment={
- <InputAdornment position="end">
- <Tooltip title='Search'>
- <IconButton
- onClick={this.handleSubmit}>
- <SearchIcon />
- </IconButton>
- </Tooltip>
- </InputAdornment>
- } />
- </FormControl>
- </form>;
+ useEffect(() => {
+ if (props.value) {
+ setValue(props.value);
}
-
- componentDidMount() {
- this.setState({
- value: this.props.value,
- label: this.props.label || 'Search'
- });
+ if (props.label) {
+ setLabel(props.label);
}
- componentWillReceiveProps(nextProps: SearchInputProps) {
- if (nextProps.value !== this.props.value) {
- this.setState({ value: nextProps.value });
- }
- if (this.state.value !== '' && nextProps.selfClearProp && nextProps.selfClearProp !== this.state.selfClearProp) {
- this.props.onSearch('');
- this.setState({ selfClearProp: nextProps.selfClearProp });
- }
- }
+ return () => {
+ setValue("");
+ clearTimeout(timeout);
+ };
+ }, [props.value, props.label]); // eslint-disable-line react-hooks/exhaustive-deps
- componentWillUnmount() {
- clearTimeout(this.timeout);
+ useEffect(() => {
+ if (selfClearProp !== props.selfClearProp) {
+ setValue("");
+ setSelfClearProp(props.selfClearProp);
+ handleChange({ target: { value: "" } } as any);
}
+ }, [props.selfClearProp]); // eslint-disable-line react-hooks/exhaustive-deps
- handleSubmit = (event: React.FormEvent<HTMLElement>) => {
- event.preventDefault();
- clearTimeout(this.timeout);
- this.props.onSearch(this.state.value);
- }
+ const handleSubmit = (event: React.FormEvent<HTMLElement>) => {
+ event.preventDefault();
+ clearTimeout(timeout);
+ props.onSearch(value);
+ };
- handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- clearTimeout(this.timeout);
- this.setState({ value: event.target.value });
- this.timeout = window.setTimeout(
- () => this.props.onSearch(this.state.value),
- this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
- );
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const { target: { value: eventValue } } = event;
+ clearTimeout(timeout);
+ setValue(eventValue);
+
+ setTimeout(window.setTimeout(
+ () => {
+ props.onSearch(eventValue);
+ },
+ props.debounce || DEFAULT_SEARCH_DEBOUNCE
+ ));
+ };
- }
- }
-);
+ return <form onSubmit={handleSubmit}>
+ <FormControl>
+ <InputLabel>{label}</InputLabel>
+ <Input
+ type="text"
+ data-cy="search-input"
+ value={value}
+ onChange={handleChange}
+ endAdornment={
+ <InputAdornment position="end">
+ <Tooltip title='Search'>
+ <IconButton
+ onClick={handleSubmit}>
+ <SearchIcon />
+ </IconButton>
+ </Tooltip>
+ </InputAdornment>
+ } />
+ </FormControl>
+ </form>;
+}
+
+export const SearchInput = withStyles(styles)(SearchInputComponent);
\ No newline at end of file
import React from 'react';
import { List, ListItem, ListItemIcon, Checkbox, Radio, Collapse } from "@material-ui/core";
import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
-import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon, FilterGroupIcon } from 'components/icon/icon';
+import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon, FilterGroupIcon, FreezeIcon } from 'components/icon/icon';
import { ReactElement } from "react";
import CircularProgress from '@material-ui/core/CircularProgress';
import classnames from "classnames";
| 'toggableIcon'
| 'checkbox'
| 'childItem'
- | 'childItemIcon';
+ | 'childItemIcon'
+ | 'frozenIcon';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
list: {
active: {
color: theme.palette.primary.main,
},
+ frozenIcon: {
+ fontSize: 20,
+ color: theme.palette.grey["600"],
+ marginLeft: '10px',
+ },
});
export enum TreeItemStatus {
flatTree?: boolean;
status: TreeItemStatus;
items?: Array<TreeItem<T>>;
+ isFrozen?: boolean;
}
export interface TreeProps<T> {
<span style={{ fontSize: '0.875rem' }}>
{item.data.name}
</span>
+ {
+ !!item.data.frozenByUuid ? <FreezeIcon className={props.classes.frozenIcon} /> : null
+ }
</span>
</div>
</div>)
: () => this.props.showSelection ? true : false;
const { levelIndentation = 20, itemRightPadding = 20 } = this.props;
-
return <List className={list}>
{items && items.map((it: TreeItem<T>, idx: number) =>
<div key={`item/${level}/${it.id}`}>
import servicesProvider from 'common/service-provider';
import { addMenuActionSet, ContextMenuKind } from 'views-components/context-menu/context-menu';
import { rootProjectActionSet } from "views-components/context-menu/action-sets/root-project-action-set";
-import { filterGroupActionSet, projectActionSet, readOnlyProjectActionSet } from "views-components/context-menu/action-sets/project-action-set";
+import { filterGroupActionSet, frozenActionSet, projectActionSet, readOnlyProjectActionSet } from "views-components/context-menu/action-sets/project-action-set";
import { resourceActionSet } from 'views-components/context-menu/action-sets/resource-action-set';
import { favoriteActionSet } from "views-components/context-menu/action-sets/favorite-action-set";
import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from 'views-components/context-menu/action-sets/collection-files-action-set';
import { groupMemberActionSet } from 'views-components/context-menu/action-sets/group-member-action-set';
import { linkActionSet } from 'views-components/context-menu/action-sets/link-action-set';
import { loadFileViewersConfig } from 'store/file-viewers/file-viewers-actions';
-import { filterGroupAdminActionSet, projectAdminActionSet } from 'views-components/context-menu/action-sets/project-admin-action-set';
+import { filterGroupAdminActionSet, frozenAdminActionSet, projectAdminActionSet } from 'views-components/context-menu/action-sets/project-admin-action-set';
import { permissionEditActionSet } from 'views-components/context-menu/action-sets/permission-edit-action-set';
import { workflowActionSet } from 'views-components/context-menu/action-sets/workflow-action-set';
import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
addMenuActionSet(ContextMenuKind.COLLECTION_ADMIN, collectionAdminActionSet);
addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet);
addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
+addMenuActionSet(ContextMenuKind.FROZEN_PROJECT, frozenActionSet);
+addMenuActionSet(ContextMenuKind.FROZEN_PROJECT_ADMIN, frozenAdminActionSet);
addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
addMenuActionSet(ContextMenuKind.PERMISSION_EDIT, permissionEditActionSet);
addMenuActionSet(ContextMenuKind.WORKFLOW, workflowActionSet);
const store = configureStore(history, services, config);
+ servicesProvider.setStore(store);
+
store.subscribe(initListener(history, store, services, config));
store.dispatch(initAuth(config));
store.dispatch(setBuildInfo());
import { GroupClass, GroupResource } from "./group";
export interface ProjectResource extends GroupResource {
+ frozenByUuid: null|string;
+ canManage: boolean;
groupClass: GroupClass.PROJECT | GroupClass.FILTER | GroupClass.ROLE;
}
}
public addFullTextSearch(value: string) {
- const terms = value.trim().split(/(\s+)/);
+ const regex = /"[^"]*"/;
+ const matches: any[] = [];
+
+ let match = value.match(regex);
+
+ while (match) {
+ value = value.replace(match[0], "");
+ matches.push(match[0].replace(/"/g, ''));
+ match = value.match(regex);
+ }
+
+ const terms = value.trim().split(/(\s+)/).concat(matches);
terms.forEach(term => {
if (term !== " ") {
this.addCondition("any", "ilike", term, "%", "%");
await commonResourceService.list({ filters: tooBig });
expect(axiosMock.history.get.length).toBe(0);
expect(axiosMock.history.post.length).toBe(1);
- expect(axiosMock.history.post[0].data.get('filters')).toBe(`[${tooBig}]`);
- expect(axiosMock.history.post[0].params._method).toBe('GET');
+ const postParams = new URLSearchParams(axiosMock.history.post[0].data);
+ expect(postParams.get('filters')).toBe(`[${tooBig}]`);
+ expect(postParams.get('_method')).toBe('GET');
});
it("#list using GET when query string is not too big", async () => {
);
} else {
// Using the POST special case to avoid URI length 414 errors.
- const formData = new FormData();
+ // We must use urlencoded post body since api doesn't support form data
+ // const formData = new FormData();
+ const formData = new URLSearchParams();
formData.append("_method", "GET");
Object.keys(params).forEach(key => {
if (params[key] !== undefined) {
}
});
return CommonService.defaultResponse(
- this.serverApi.post(`/${this.resourceType}`, formData, {
- params: {
- _method: 'GET'
- }
- }),
+ this.serverApi.post(`/${this.resourceType}`, formData, {}),
this.actions,
showErrors
);
export const ADVANCED_TAB_DIALOG = 'advancedTabDialog';
-interface AdvancedTabDialogData {
- apiResponse: any;
+export interface AdvancedTabDialogData {
+ uuid: string;
+ apiResponse: JSX.Element;
metadata: ListResults<LinkResource> | string;
user: UserResource | string;
pythonHeader: string;
uuid: string;
metadata: ListResults<LinkResource> | string;
user: UserResource | string;
- apiResponseKind: any;
+ apiResponseKind: (apiResponse) => JSX.Element;
data: AdvanceResponseData;
resourceKind: AdvanceResourceKind;
resourcePrefix: AdvanceResourcePrefix;
const stringifyObject = (item: any) =>
JSON.stringify(item, null, 2) || 'null';
-const containerRequestApiResponse = (apiResponse: ContainerRequestResource) => {
+const containerRequestApiResponse = (apiResponse: ContainerRequestResource): JSX.Element => {
const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, properties, state, requestingContainerUuid, containerUuid,
containerCountMax, mounts, runtimeConstraints, containerImage, environment, cwd, command, outputPath, priority, expiresAt, filters, containerCount,
useExisting, schedulingParameters, outputUuid, logUuid, outputName, outputTtl } = apiResponse;
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
-const collectionApiResponse = (apiResponse: CollectionResource) => {
+const collectionApiResponse = (apiResponse: CollectionResource): JSX.Element => {
const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, properties, portableDataHash, replicationDesired,
replicationConfirmedAt, replicationConfirmed, deleteAt, trashAt, isTrashed, storageClassesDesired,
storageClassesConfirmed, storageClassesConfirmedAt, currentVersionUuid, version, preserveVersion, fileCount, fileSizeTotal } = apiResponse;
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
-const groupRequestApiResponse = (apiResponse: ProjectResource) => {
+const groupRequestApiResponse = (apiResponse: ProjectResource): JSX.Element => {
const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, groupClass, trashAt, isTrashed, deleteAt, properties, writableBy } = apiResponse;
const response = `
"uuid": "${uuid}",
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
-const repositoryApiResponse = (apiResponse: RepositoryResource) => {
+const repositoryApiResponse = (apiResponse: RepositoryResource): JSX.Element => {
const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, cloneUrls } = apiResponse;
const response = `
"uuid": "${uuid}",
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
-const sshKeyApiResponse = (apiResponse: SshKeyResource) => {
+const sshKeyApiResponse = (apiResponse: SshKeyResource): JSX.Element => {
const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, authorizedUserUuid, expiresAt } = apiResponse;
const response = `
"uuid": "${uuid}",
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
-const virtualMachineApiResponse = (apiResponse: VirtualMachinesResource) => {
+const virtualMachineApiResponse = (apiResponse: VirtualMachinesResource): JSX.Element => {
const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, hostname } = apiResponse;
const response = `
"hostname": ${stringify(hostname)},
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
-const keepServiceApiResponse = (apiResponse: KeepServiceResource) => {
+const keepServiceApiResponse = (apiResponse: KeepServiceResource): JSX.Element => {
const {
uuid, readOnly, serviceHost, servicePort, serviceSslFlag, serviceType,
ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
-const userApiResponse = (apiResponse: UserResource) => {
+const userApiResponse = (apiResponse: UserResource): JSX.Element => {
const {
uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid,
email, firstName, lastName, username, isActive, isAdmin, prefs, defaultOwnerUuid,
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
-const apiClientAuthorizationApiResponse = (apiResponse: ApiClientAuthorization) => {
+const apiClientAuthorizationApiResponse = (apiResponse: ApiClientAuthorization): JSX.Element => {
const {
uuid, ownerUuid, apiToken, apiClientId, userId, createdByIpAddress, lastUsedByIpAddress,
lastUsedAt, expiresAt, defaultOwnerUuid, scopes, updatedAt, createdAt
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
-const linkApiResponse = (apiResponse: LinkResource) => {
+const linkApiResponse = (apiResponse: LinkResource): JSX.Element => {
const {
uuid, name, headUuid, properties, headKind, tailUuid, tailKind, linkClass,
ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid
import { GroupClass, GroupResource } from 'models/group';
import { GroupContentsResource } from 'services/groups-service/groups-service';
import { LinkResource } from 'models/link';
+import { resourceIsFrozen } from 'common/frozen-resources';
+import { ProjectResource } from 'models/project';
export const contextMenuActions = unionize({
OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
isEditable?: boolean;
outputUuid?: string;
workflowUuid?: string;
+ isAdmin?: boolean;
+ isFrozen?: boolean;
storageClassesDesired?: string[];
properties?: { [key: string]: string | string[] };
};
description: res.description,
ownerUuid: res.ownerUuid,
isTrashed: ('isTrashed' in res) ? res.isTrashed : false,
+ isFrozen: !!(res as ProjectResource).frozenByUuid,
}));
}
};
const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
const kind = extractUuidKind(uuid);
const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
+ const isFrozen = resourceIsFrozen(resource, getState().resources);
+ const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly && !isFrozen;
- const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly;
switch (kind) {
case ResourceKind.PROJECT:
+ if (isFrozen) {
+ return isAdminUser ? ContextMenuKind.FROZEN_PROJECT_ADMIN : ContextMenuKind.FROZEN_PROJECT;
+ }
+
return (isAdminUser && !readonly)
? (resource && resource.groupClass !== GroupClass.FILTER)
? ContextMenuKind.PROJECT_ADMIN
? ContextMenuKind.OLD_VERSION_COLLECTION
: (isTrashed && isEditable)
? ContextMenuKind.TRASHED_COLLECTION
- : (isAdminUser && !readonly)
+ : (isAdminUser && isEditable)
? ContextMenuKind.COLLECTION_ADMIN
: isEditable
? ContextMenuKind.COLLECTION
: ContextMenuKind.READONLY_COLLECTION;
case ResourceKind.PROCESS:
- return (isAdminUser && !readonly)
+ return (isAdminUser && isEditable)
? ContextMenuKind.PROCESS_ADMIN
: readonly
? ContextMenuKind.READONLY_PROCESS_RESOURCE
// Copy to clipboard omits token to avoid accidental sharing
const url = getNavUrl(resource.uuid, getState().auth, false);
- if (url) {
+ if (url[0] === '/') {
+ copy(`${window.location.origin}${url}`);
+ } else if (url.length) {
copy(url);
}
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { ServiceRepository } from "services/services";
+import { projectPanelActions } from "store/project-panel/project-panel-action";
+import { loadResource } from "store/resources/resources-actions";
+import { RootState } from "store/store";
+
+export const freezeProject = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const userUUID = getState().auth.user!.uuid;
+
+ const updatedProject = await services.projectService.update(uuid, {
+ frozenByUuid: userUUID
+ });
+
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch<any>(loadResource(uuid, false));
+ return updatedProject;
+ };
+
+export const unfreezeProject = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+
+ const updatedProject = await services.projectService.update(uuid, {
+ frozenByUuid: null
+ });
+
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch<any>(loadResource(uuid, false));
+ return updatedProject;
+ };
\ No newline at end of file
const { items: permissionLinks } = await permissionService.listResourcePermissions(resourceUuid);
dispatch<any>(initializePublicAccessForm(permissionLinks));
const filters = new FilterBuilder()
- .addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
+ .addIn('uuid', Array.from(new Set(permissionLinks.map(({ tailUuid }) => tailUuid))))
.getFilters();
- const { items: users } = await userService.list({ filters, count: "none" });
- const { items: groups } = await groupsService.list({ filters, count: "none" });
+ const { items: users } = await userService.list({ filters, count: "none", limit: 1000 });
+ const { items: groups } = await groupsService.list({ filters, count: "none", limit: 1000 });
const getEmail = (tailUuid: string) => {
const user = users.find(({ uuid }) => uuid === tailUuid);
export const snackbarActions = unionize({
OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number, kind?: SnackbarKind, link?: string}>(),
- CLOSE_SNACKBAR: ofType<{}>(),
+ CLOSE_SNACKBAR: ofType<{}|null>(),
SHIFT_MESSAGES: ofType<{}>()
});
})
};
},
- CLOSE_SNACKBAR: () => ({
- ...state,
- open: false
- }),
+ CLOSE_SNACKBAR: (payload) => {
+ let newMessages: any = [...state.messages];// state.messages.filter(({ message }) => message !== payload);
+
+ if (payload === undefined || JSON.stringify(payload) === '{}') {
+ newMessages.pop();
+ } else {
+ newMessages = state.messages.filter((message, index) => index !== payload);
+ }
+
+ return {
+ ...state,
+ messages: newMessages,
+ open: newMessages.length > 0
+ }
+ },
SHIFT_MESSAGES: () => {
const messages = state.messages.filter((m, idx) => idx > 0);
return {
includeFiles?: boolean;
includeFilterGroups?: boolean;
loadShared?: boolean;
+ options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
}
export const loadProject = (params: LoadProjectParams) =>
async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
- const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false } = params;
+ const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options } = params;
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
)(new FilterBuilder());
const { items } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined });
-
dispatch<any>(receiveTreePickerData<GroupContentsResource>({
id,
pickerId,
if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
return false;
}
+
+ if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
+ return false;
+ }
+
return true;
}),
extractNodeData: item => ({
}));
}
};
-export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false) =>
+export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean } ) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const uuid = getUserUuid(getState());
if (uuid) {
- dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles }));
+ dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles, options }));
}
};
pickerId: string;
includeCollections?: boolean;
includeFiles?: boolean;
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}
export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
return false;
}
+ if (options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
+ return false;
+ }
+
return true;
}),
extractNodeData: item => ({
dispatch<any>(receiveTreePickerData<LinkResource>({
id: 'Public Favorites',
pickerId,
- data: items,
+ data: items.filter(item => {
+ if (params.options && params.options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as any).frozenByUuid) {
+ return false;
+ }
+
+ return true;
+ }),
extractNodeData: item => ({
id: item.headUuid,
value: item,
filters: new FilterBuilder()
.addIn('head_uuid', virtualMachines.items.map(item => item.uuid))
.addEqual('name', PermissionLevel.CAN_LOGIN)
- .getFilters()
+ .getFilters(),
+ limit: 1000
});
dispatch(updateResources(logins.items));
dispatch(virtualMachinesActions.SET_LINKS(logins));
filters: new FilterBuilder()
.addIn('uuid', logins.items.map(item => item.tailUuid))
.getFilters(),
- count: "none"
+ count: "none", // Necessary for federated queries
+ limit: 1000
});
dispatch(updateResources(users.items));
export const loadVirtualMachinesUserData = () =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch<any>(loadRequestedDate());
+ const user = getState().auth.user;
const virtualMachines = await services.virtualMachineService.list();
const virtualMachinesUuids = virtualMachines.items.map(it => it.uuid);
const links = await services.linkService.list({
filters: new FilterBuilder()
.addIn("head_uuid", virtualMachinesUuids)
+ .addEqual("tail_uuid", user?.uuid)
.getFilters()
});
dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
import { WithDialogProps } from 'store/dialog/with-dialog';
import { withDialog } from "store/dialog/with-dialog";
import { compose } from 'redux';
-import { ADVANCED_TAB_DIALOG } from "store/advanced-tab/advanced-tab";
+import { AdvancedTabDialogData, ADVANCED_TAB_DIALOG } from "store/advanced-tab/advanced-tab";
import { DefaultCodeSnippet } from "components/default-code-snippet/default-code-snippet";
import { MetadataTab } from 'views-components/advanced-tab-dialog/metadataTab';
+import { LinkResource } from "models/link";
+import { ListResults } from "services/common-service/common-service";
type CssRules = 'content' | 'codeSnippet' | 'spacing';
withDialog(ADVANCED_TAB_DIALOG),
withStyles(styles),
)(
- class extends React.Component<WithDialogProps<any> & WithStyles<CssRules>>{
+ class extends React.Component<WithDialogProps<AdvancedTabDialogData> & WithStyles<CssRules>>{
state = {
value: 0,
};
maxWidth="lg"
onClose={closeDialog}
onExit={() => this.setState({ value: 0 })} >
- <DialogTitle>Advanced</DialogTitle>
+ <DialogTitle>API Details</DialogTitle>
<Tabs value={value} onChange={this.handleChange} fullWidth>
<Tab label="API RESPONSE" />
<Tab label="METADATA" />
<DialogContent className={classes.content}>
{value === 0 && <div>{dialogContentExample(apiResponse, classes)}</div>}
{value === 1 && <div>
- {metadata !== '' && metadata.items.length > 0 ?
- <MetadataTab items={metadata.items} uuid={uuid} />
+ {metadata !== '' && (metadata as ListResults<LinkResource>).items.length > 0 ?
+ <MetadataTab items={(metadata as ListResults<LinkResource>).items} uuid={uuid} />
: dialogContentHeader('(No metadata links found)')}
</div>}
{value === 2 && dialogContent(pythonHeader, pythonExample, classes)}
{header}
</DialogContentText>;
-const dialogContentExample = (example: string, classes: any) =>
- <DefaultCodeSnippet
+const dialogContentExample = (example: JSX.Element | string, classes: any) => {
+ // Pass string to lines param or JSX to child props
+ const stringData = example && (example as string).length ? (example as string) : undefined;
+ return <DefaultCodeSnippet
apiResponse
className={classes.codeSnippet}
- lines={[example]} />;
\ No newline at end of file
+ lines={stringData ? [stringData] : []}
+ >
+ {example as JSX.Element || null}
+ </DefaultCodeSnippet>;
+}
// SPDX-License-Identifier: AGPL-3.0
import { connect } from "react-redux";
-import { Breadcrumbs as BreadcrumbsComponent, BreadcrumbsProps } from 'components/breadcrumbs/breadcrumbs';
+import { Breadcrumb, Breadcrumbs as BreadcrumbsComponent, BreadcrumbsProps } from 'components/breadcrumbs/breadcrumbs';
import { RootState } from 'store/store';
import { Dispatch } from 'redux';
import { navigateTo } from 'store/navigation/navigation-action';
import { getProperty } from '../../store/properties/properties';
import { ResourceBreadcrumb, BREADCRUMBS } from '../../store/breadcrumbs/breadcrumbs-actions';
import { openSidePanelContextMenu } from 'store/context-menu/context-menu-actions';
+import { ProjectResource } from "models/project";
-type BreadcrumbsDataProps = Pick<BreadcrumbsProps, 'items'>;
+type BreadcrumbsDataProps = Pick<BreadcrumbsProps, 'items' | 'resources'>;
type BreadcrumbsActionProps = Pick<BreadcrumbsProps, 'onClick' | 'onContextMenu'>;
-const mapStateToProps = () => ({ properties }: RootState): BreadcrumbsDataProps => ({
- items: getProperty<ResourceBreadcrumb[]>(BREADCRUMBS)(properties) || []
+const mapStateToProps = () => ({ properties, resources }: RootState): BreadcrumbsDataProps => ({
+ items: (getProperty<ResourceBreadcrumb[]>(BREADCRUMBS)(properties) || []),
+ resources,
});
const mapDispatchToProps = (dispatch: Dispatch): BreadcrumbsActionProps => ({
- onClick: ({ uuid }: ResourceBreadcrumb) => {
+ onClick: ({ uuid }: Breadcrumb & ProjectResource) => {
dispatch<any>(navigateTo(uuid));
},
- onContextMenu: (event, breadcrumb: ResourceBreadcrumb) => {
+ onContextMenu: (event, breadcrumb: Breadcrumb & ProjectResource) => {
dispatch<any>(openSidePanelContextMenu(event, breadcrumb.uuid));
}
});
dispatch<any>(openApiClientAuthorizationAttributesDialog(uuid));
}
}, {
- name: "Advanced",
+ name: "API Details",
icon: AdvancedIcon,
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
},
{
icon: AdvancedIcon,
- name: "Advanced",
+ name: "API Details",
execute: (dispatch, resource) => {
dispatch<any>(openAdvancedTabDialog(resource.uuid));
}
dispatch<any>(openGroupAttributes(uuid));
}
}, {
- name: "Advanced",
+ name: "API Details",
icon: AdvancedIcon,
execute: (dispatch, resource) => {
dispatch<any>(openAdvancedTabDialog(resource.uuid));
dispatch<any>(openGroupMemberAttributes(uuid));
}
}, {
- name: "Advanced",
+ name: "API Details",
icon: AdvancedIcon,
execute: (dispatch, resource) => {
dispatch<any>(openAdvancedTabDialog(resource.uuid));
execute: (dispatch, { uuid }) => {
dispatch<any>(openRemoveGroupMemberDialog(uuid));
}
-}]];
\ No newline at end of file
+}]];
dispatch<any>(openKeepServiceAttributesDialog(uuid));
}
}, {
- name: "Advanced",
+ name: "API Details",
icon: AdvancedIcon,
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
dispatch<any>(openLinkAttributesDialog(uuid));
}
}, {
- name: "Advanced",
+ name: "API Details",
icon: AdvancedIcon,
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
},
{
icon: AdvancedIcon,
- name: "Advanced",
+ name: "API Details",
execute: (dispatch, resource) => {
dispatch<any>(openAdvancedTabDialog(resource.uuid));
}
import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
import { openWebDavS3InfoDialog } from "store/collections/collection-info-actions";
+import { ToggleLockAction } from "../actions/lock-action";
+import { freezeProject, unfreezeProject } from "store/projects/project-lock-actions";
-export const readOnlyProjectActionSet: ContextMenuActionSet = [[
- {
- component: ToggleFavoriteAction,
- name: 'ToggleFavoriteAction',
- execute: (dispatch, resource) => {
- dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
- });
- }
- },
- {
- icon: OpenIcon,
- name: "Open in new tab",
- execute: (dispatch, resource) => {
- dispatch<any>(openInNewTabAction(resource));
- }
- },
- {
- icon: Link,
- name: "Copy to clipboard",
- execute: (dispatch, resource) => {
- dispatch<any>(copyToClipboardAction(resource));
- }
- },
- {
- icon: DetailsIcon,
- name: "View details",
- execute: dispatch => {
- dispatch<any>(toggleDetailsPanel());
- }
- },
- {
- icon: AdvancedIcon,
- name: "Advanced",
- execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid));
- }
- },
- {
- icon: FolderSharedIcon,
- name: "Open with 3rd party client",
- execute: (dispatch, resource) => {
- dispatch<any>(openWebDavS3InfoDialog(resource.uuid));
+export const toggleFavoriteAction = {
+ component: ToggleFavoriteAction,
+ name: 'ToggleFavoriteAction',
+ execute: (dispatch, resource) => {
+ dispatch(toggleFavorite(resource)).then(() => {
+ dispatch(favoritePanelActions.REQUEST_ITEMS());
+ });
+ }
+};
+
+export const openInNewTabMenuAction = {
+ icon: OpenIcon,
+ name: "Open in new tab",
+ execute: (dispatch, resource) => {
+ dispatch(openInNewTabAction(resource));
+ }
+};
+
+export const copyToClipboardMenuAction = {
+ icon: Link,
+ name: "Copy to clipboard",
+ execute: (dispatch, resource) => {
+ dispatch(copyToClipboardAction(resource));
+ }
+};
+
+export const viewDetailsAction = {
+ icon: DetailsIcon,
+ name: "View details",
+ execute: dispatch => {
+ dispatch(toggleDetailsPanel());
+ }
+}
+
+export const advancedAction = {
+ icon: AdvancedIcon,
+ name: "API Details",
+ execute: (dispatch, resource) => {
+ dispatch(openAdvancedTabDialog(resource.uuid));
+ }
+}
+
+export const openWith3rdPartyClientAction = {
+ icon: FolderSharedIcon,
+ name: "Open with 3rd party client",
+ execute: (dispatch, resource) => {
+ dispatch(openWebDavS3InfoDialog(resource.uuid));
+ }
+}
+
+export const editProjectAction = {
+ icon: RenameIcon,
+ name: "Edit project",
+ execute: (dispatch, resource) => {
+ dispatch(openProjectUpdateDialog(resource));
+ }
+}
+
+export const shareAction = {
+ icon: ShareIcon,
+ name: "Share",
+ execute: (dispatch, { uuid }) => {
+ dispatch(openSharingDialog(uuid));
+ }
+}
+
+export const moveToAction = {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resource) => {
+ dispatch(openMoveProjectDialog(resource));
+ }
+}
+
+export const toggleTrashAction = {
+ component: ToggleTrashAction,
+ name: 'ToggleTrashAction',
+ execute: (dispatch, resource) => {
+ dispatch(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
+ }
+}
+
+export const freezeProjectAction = {
+ component: ToggleLockAction,
+ name: 'ToggleLockAction',
+ execute: (dispatch, resource) => {
+ if (resource.isFrozen) {
+ dispatch(unfreezeProject(resource.uuid));
+ } else {
+ dispatch(freezeProject(resource.uuid));
}
- },
+ }
+}
+
+export const newProjectAction: any = {
+ icon: NewProjectIcon,
+ name: "New project",
+ execute: (dispatch, resource): void => {
+ dispatch(openProjectCreateDialog(resource.uuid));
+ }
+}
+
+export const readOnlyProjectActionSet: ContextMenuActionSet = [[
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+]];
+
+export const filterGroupActionSet: ContextMenuActionSet = [[
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ editProjectAction,
+ shareAction,
+ moveToAction,
+ toggleTrashAction,
]];
-export const filterGroupActionSet: ContextMenuActionSet = [
- [
- ...readOnlyProjectActionSet.reduce((prev, next) => prev.concat(next), []),
- {
- icon: RenameIcon,
- name: "Edit project",
- execute: (dispatch, resource) => {
- dispatch<any>(openProjectUpdateDialog(resource));
- }
- },
- {
- icon: ShareIcon,
- name: "Share",
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSharingDialog(uuid));
- }
- },
- {
- icon: MoveToIcon,
- name: "Move to",
- execute: (dispatch, resource) => {
- dispatch<any>(openMoveProjectDialog(resource));
- }
- },
- {
- component: ToggleTrashAction,
- name: 'ToggleTrashAction',
- execute: (dispatch, resource) => {
- dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
- }
- },
- ]
-];
-
-export const projectActionSet: ContextMenuActionSet = [
- [
- ...filterGroupActionSet.reduce((prev, next) => prev.concat(next), []),
- {
- icon: NewProjectIcon,
- name: "New project",
- execute: (dispatch, resource) => {
- dispatch<any>(openProjectCreateDialog(resource.uuid));
- }
- },
- ]
-];
+export const frozenActionSet: ContextMenuActionSet = [[
+ shareAction,
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ freezeProjectAction
+]];
+
+export const projectActionSet: ContextMenuActionSet = [[
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ editProjectAction,
+ shareAction,
+ moveToAction,
+ toggleTrashAction,
+ newProjectAction,
+ freezeProjectAction,
+]];
import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
import { publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
-import { projectActionSet, filterGroupActionSet } from "views-components/context-menu/action-sets/project-action-set";
+import { shareAction, toggleFavoriteAction, openInNewTabMenuAction, copyToClipboardMenuAction, viewDetailsAction, advancedAction, openWith3rdPartyClientAction, freezeProjectAction, editProjectAction, moveToAction, toggleTrashAction, newProjectAction } from "views-components/context-menu/action-sets/project-action-set";
+
+export const togglePublicFavoriteAction = {
+ component: TogglePublicFavoriteAction,
+ name: 'TogglePublicFavoriteAction',
+ execute: (dispatch, resource) => {
+ dispatch(togglePublicFavorite(resource)).then(() => {
+ dispatch(publicFavoritePanelActions.REQUEST_ITEMS());
+ });
+}}
export const projectAdminActionSet: ContextMenuActionSet = [[
- ...projectActionSet.reduce((prev, next) => prev.concat(next), []),
- {
- component: TogglePublicFavoriteAction,
- name: 'TogglePublicFavoriteAction',
- execute: (dispatch, resource) => {
- dispatch<any>(togglePublicFavorite(resource)).then(() => {
- dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
- });
- }
- }
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ editProjectAction,
+ shareAction,
+ moveToAction,
+ toggleTrashAction,
+ newProjectAction,
+ freezeProjectAction,
+ togglePublicFavoriteAction
]];
export const filterGroupAdminActionSet: ContextMenuActionSet = [[
- ...filterGroupActionSet.reduce((prev, next) => prev.concat(next), []),
- {
- component: TogglePublicFavoriteAction,
- name: 'TogglePublicFavoriteAction',
- execute: (dispatch, resource) => {
- dispatch<any>(togglePublicFavorite(resource)).then(() => {
- dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
- });
- }
- }
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ editProjectAction,
+ shareAction,
+ moveToAction,
+ toggleTrashAction,
+ togglePublicFavoriteAction
+]];
+
+
+export const frozenAdminActionSet: ContextMenuActionSet = [[
+ shareAction,
+ togglePublicFavoriteAction,
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ freezeProjectAction
]];
dispatch<any>(openSharingDialog(uuid));
}
}, {
- name: "Advanced",
+ name: "API Details",
icon: AdvancedIcon,
execute: (dispatch, resource) => {
dispatch<any>(openAdvancedTabDialog(resource.uuid));
},
{
icon: AdvancedIcon,
- name: "Advanced",
+ name: "API Details",
execute: (dispatch, resource) => {
dispatch<any>(openAdvancedTabDialog(resource.uuid));
}
dispatch<any>(openSshKeyAttributesDialog(uuid));
}
}, {
- name: "Advanced",
+ name: "API Details",
icon: AdvancedIcon,
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
},
{
icon: AdvancedIcon,
- name: "Advanced",
+ name: "API Details",
execute: (dispatch, resource) => {
dispatch<any>(openAdvancedTabDialog(resource.uuid));
}
dispatch<any>(openUserProjects(uuid));
}
}, {
- name: "Advanced",
+ name: "API Details",
icon: AdvancedIcon,
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
dispatch<any>(openVirtualMachineAttributes(uuid));
}
}, {
- name: "Advanced",
+ name: "API Details",
icon: AdvancedIcon,
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
+import { FreezeIcon, UnfreezeIcon } from "components/icon/icon";
+import { connect } from "react-redux";
+import { RootState } from "store/store";
+import { ProjectResource } from "models/project";
+import { withRouter, RouteComponentProps } from "react-router";
+import { resourceIsFrozen } from "common/frozen-resources";
+
+const mapStateToProps = (state: RootState, props: { onClick: () => {} }) => ({
+ isAdmin: !!state.auth.user?.isAdmin,
+ isLocked: !!(state.resources[state.contextMenu.resource!.uuid] as ProjectResource).frozenByUuid,
+ canManage: (state.resources[state.contextMenu.resource!.uuid] as ProjectResource).canManage,
+ canUnfreeze: !state.auth.remoteHostsConfig[state.auth.homeCluster]?.clusterConfig?.API?.UnfreezeProjectRequiresAdmin,
+ resource: state.contextMenu.resource,
+ resources: state.resources,
+ onClick: props.onClick
+});
+
+export const ToggleLockAction = withRouter(connect(mapStateToProps)((props: {
+ resource: any,
+ resources: any,
+ onClick: () => void,
+ state: RootState, isAdmin: boolean, isLocked: boolean, canManage: boolean, canUnfreeze: boolean,
+} & RouteComponentProps) =>
+ (props.canManage && !props.isLocked) || (props.isLocked && props.canManage && (props.canUnfreeze || props.isAdmin)) ?
+ resourceIsFrozen(props.resource, props.resources) ? null :
+ <ListItem
+ button
+ onClick={props.onClick} >
+ <ListItemIcon>
+ {props.isLocked
+ ? <UnfreezeIcon />
+ : <FreezeIcon />}
+ </ListItemIcon>
+ <ListItemText style={{ textDecoration: 'none' }}>
+ {props.isLocked
+ ? <>Unfreeze project</>
+ : <>Freeze project</>}
+ </ListItemText>
+ </ListItem > : null));
import { Dispatch } from "redux";
import { ContextMenuItem } from "components/context-menu/context-menu";
import { ContextMenuResource } from "store/context-menu/context-menu-actions";
+import { RootState } from "store/store";
export interface ContextMenuAction extends ContextMenuItem {
- execute(dispatch: Dispatch, resource: ContextMenuResource): void;
+ execute(dispatch: Dispatch, resource: ContextMenuResource, state?: any): void;
}
export type ContextMenuActionSet = Array<Array<ContextMenuAction>>;
PROJECT = "Project",
FILTER_GROUP = "FilterGroup",
READONLY_PROJECT = 'ReadOnlyProject',
+ FROZEN_PROJECT = 'FrozenProject',
+ FROZEN_PROJECT_ADMIN = 'FrozenProjectAdmin',
PROJECT_ADMIN = "ProjectAdmin",
FILTER_GROUP_ADMIN = "FilterGroupAdmin",
RESOURCE = "Resource",
import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
import { Resource, ResourceKind, TrashableResource } from 'models/resource';
import {
+ FreezeIcon,
ProjectIcon,
FilterGroupIcon,
CollectionIcon,
import { getUserUuid } from 'common/getuser';
import { VirtualMachinesResource } from 'models/virtual-machines';
import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
+import { ProjectResource } from 'models/project';
const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
<Typography variant="caption">
<FavoriteStar resourceUuid={item.uuid} />
<PublicFavoriteStar resourceUuid={item.uuid} />
+ {
+ item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />
+ }
</Typography>
</Grid>
</Grid>;
};
+const FrozenProject = (props: {item: ProjectResource}) => {
+ const [fullUsername, setFullusername] = React.useState<any>(null);
+ const getFullName = React.useCallback(() => {
+ if (props.item.frozenByUuid) {
+ setFullusername(<UserNameFromID uuid={props.item.frozenByUuid} />);
+ }
+ }, [props.item, setFullusername])
+
+ if (props.item.frozenByUuid) {
+
+ return <Tooltip onOpen={getFullName} enterDelay={500} title={<span>Project was frozen by {fullUsername}</span>}>
+ <FreezeIcon style={{ fontSize: "inherit" }}/>
+ </Tooltip>;
+ } else {
+ return null;
+ }
+}
+
export const ResourceName = connect(
(state: RootState, props: { uuid: string }) => {
const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
export const UserNameFromID =
compose(userFromID)(
- (props: { uuid: string, userFullname: string, dispatch: Dispatch }) => {
+ (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
const { uuid, userFullname, dispatch } = props;
if (userFullname === '') {
import { DetailsData } from "./details-data";
import { CollectionDetailsAttributes } from 'views/collection-panel/collection-panel';
import { RootState } from 'store/store';
-import { filterResources, getResource } from 'store/resources/resources';
+import { filterResources, getResource, ResourcesState } from 'store/resources/resources';
import { connect } from 'react-redux';
import { Button, Grid, ListItem, StyleRulesCallback, Typography, withStyles, WithStyles } from '@material-ui/core';
import { formatDate, formatFileSize } from 'common/formatters';
import { navigateTo } from 'store/navigation/navigation-action';
import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
import { openCollectionUpdateDialog } from 'store/collections/collection-update-actions';
+import { resourceIsFrozen } from 'common/frozen-resources';
export type CssRules = 'versionBrowserHeader'
| 'versionBrowserItem'
}
interface CollectionInfoDataProps {
+ resources: ResourcesState;
currentCollection: CollectionResource | undefined;
}
const ciMapStateToProps = (state: RootState): CollectionInfoDataProps => {
return {
+ resources: state.resources,
currentCollection: getResource<CollectionResource>(state.detailsPanel.resourceUuid)(state.resources),
};
};
const CollectionInfo = withStyles(styles)(
connect(ciMapStateToProps, ciMapDispatchToProps)(
- ({ currentCollection, editCollection, classes }: CollectionInfoProps) =>
+ ({ currentCollection, resources, editCollection, classes }: CollectionInfoProps) =>
currentCollection !== undefined
? <div>
<Button
+ disabled={resourceIsFrozen(currentCollection, resources)}
className={classes.editButton} variant='contained'
data-cy='details-panel-edit-btn' color='primary' size='small'
onClick={() => editCollection(currentCollection)}>
import { toggleDetailsPanel, SLIDE_TIMEOUT, openDetailsPanel } from 'store/details-panel/details-panel-action';
import { FileDetails } from 'views-components/details-panel/file-details';
import { getNode } from 'models/tree';
+import { resourceIsFrozen } from 'common/frozen-resources';
type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
const file = resource
? undefined
: getNode(detailsPanel.resourceUuid)(collectionPanelFiles);
+
+ let isFrozen = false;
+ if (resource) {
+ isFrozen = resourceIsFrozen(resource, resources);
+ }
+
return {
+ isFrozen,
authConfig: auth.config,
isOpened: detailsPanel.isOpened,
tabNr: detailsPanel.tabNr,
isOpened: boolean;
tabNr: number;
res: DetailsResource;
+ isFrozen: boolean;
}
type DetailsPanelProps = DetailsPanelDataProps & WithStyles<CssRules>;
import { ResourceWithName } from '../data-explorer/renderers';
import { GroupClass } from "models/group";
import { openProjectUpdateDialog, ProjectUpdateFormDialogData } from 'store/projects/project-update-actions';
+import { RootState } from 'store/store';
+import { ResourcesState } from 'store/resources/resources';
+import { resourceIsFrozen } from 'common/frozen-resources';
export class ProjectDetails extends DetailsData<ProjectResource> {
getIcon(className?: string) {
onClick: (prj: ProjectUpdateFormDialogData) => () => void;
}
+const mapStateToProps = (state: RootState): { resources: ResourcesState } => {
+ return {
+ resources: state.resources
+ };
+};
+
const mapDispatchToProps = (dispatch: Dispatch) => ({
onClick: (prj: ProjectUpdateFormDialogData) =>
() => dispatch<any>(openProjectUpdateDialog(prj)),
type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDetailsComponentActionProps & WithStyles<CssRules>;
-const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
+const ProjectDetailsComponent = connect(mapStateToProps, mapDispatchToProps)(
withStyles(styles)(
- ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
+ ({ classes, project, resources, onClick }: ProjectDetailsComponentProps & { resources: ResourcesState }) => <div>
{project.groupClass !== GroupClass.FILTER ?
<Button onClick={onClick({
uuid: project.uuid,
description: project.description,
properties: project.properties,
})}
+ disabled={resourceIsFrozen(project, resources)}
className={classes.editButton} variant='contained'
data-cy='details-panel-edit-btn' color='primary' size='small'>
<RenameIcon className={classes.editIcon} /> Edit
dispatch<any>(
data.kind === ResourceKind.COLLECTION
? loadCollection(id, pickerId)
- : loadProject({ id, pickerId, includeCollections, includeFiles })
+ : loadProject({ id, pickerId, includeCollections, includeFiles, options })
);
} else if (!('type' in data) && loadRootItem) {
loadRootItem(item as TreeItem<ProjectsTreePickerRootItem>, pickerId, includeCollections, includeFiles, options);
export const HomeTreePicker = connect(() => ({
rootItemIcon: ProjectIcon,
}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
- loadRootItem: (_, pickerId, includeCollections, includeFiles) => {
- dispatch<any>(loadUserProject(pickerId, includeCollections, includeFiles));
+ loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
+ dispatch<any>(loadUserProject(pickerId, includeCollections, includeFiles, options));
},
}))(ProjectsTreePicker);
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { values, memoize, pipe } from 'lodash/fp';
+import { values, pipe } from 'lodash/fp';
import { HomeTreePicker } from 'views-components/projects-tree-picker/home-tree-picker';
import { SharedTreePicker } from 'views-components/projects-tree-picker/shared-tree-picker';
import { FavoritesTreePicker } from 'views-components/projects-tree-picker/favorites-tree-picker';
</div>;
};
-const getRelatedTreePickers = memoize(pipe(getProjectsTreePickerIds, values));
+const getRelatedTreePickers = pipe(getProjectsTreePickerIds, values);
const disableActivation = [SHARED_PROJECT_ID, FAVORITES_PROJECT_ID];
export const PublicFavoritesTreePicker = connect(() => ({
rootItemIcon: PublicFavoriteIcon,
}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
- loadRootItem: (_, pickerId, includeCollections, includeFiles) => {
- dispatch<any>(loadPublicFavoritesProject({ pickerId, includeCollections, includeFiles }));
+ loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
+ dispatch<any>(loadPublicFavoritesProject({ pickerId, includeCollections, includeFiles, options }));
},
}))(ProjectsTreePicker);
\ No newline at end of file
export const SharedTreePicker = connect(() => ({
rootItemIcon: ShareMeIcon,
}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
- loadRootItem: (_, pickerId, includeCollections, includeFiles) => {
- dispatch<any>(loadProject({ id: 'Shared with me', pickerId, includeCollections, includeFiles, loadShared: true }));
+ loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
+ dispatch<any>(loadProject({ id: 'Shared with me', pickerId, includeCollections, includeFiles, loadShared: true, options }));
},
}))(ProjectsTreePicker);
\ No newline at end of file
import { pluginConfig } from 'plugins';
import { ElementListReducer } from 'common/plugintypes';
import { Location } from 'history';
+import { ProjectResource } from 'models/project';
type CssRules = 'button' | 'menuItem' | 'icon';
if (currentItemId === currentUserUUID) {
enabled = true;
} else if (matchProjectRoute(location ? location.pathname : '')) {
- const currentProject = getResource<GroupResource>(currentItemId)(resources);
- if (currentProject &&
+ const currentProject = getResource<ProjectResource>(currentItemId)(resources);
+ if (currentProject && currentProject.writableBy &&
currentProject.writableBy.indexOf(currentUserUUID || '') >= 0 &&
+ !currentProject.frozenByUuid &&
!isProjectTrashed(currentProject, resources) &&
currentProject.groupClass !== GroupClass.FILTER) {
enabled = true;
export const SidePanelTree = connect(undefined, mapDispatchToProps)(
(props: SidePanelTreeActionProps) =>
- <span data-cy="side-panel-tree">
+ <div data-cy="side-panel-tree">
<TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />
- </span>);
+ </div>);
const renderSidePanelItem = (item: TreeItem<ProjectResource>) => {
const name = typeof item.data === 'string' ? item.data : item.data.name;
import { RootState } from "store/store";
import { Button, IconButton, StyleRulesCallback, WithStyles, withStyles, SnackbarContent } from '@material-ui/core';
import MaterialSnackbar, { SnackbarOrigin } from "@material-ui/core/Snackbar";
-import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind, SnackbarMessage } from "store/snackbar/snackbar-actions";
import { navigateTo } from 'store/navigation/navigation-action';
import WarningIcon from '@material-ui/icons/Warning';
import CheckCircleIcon from '@material-ui/icons/CheckCircle';
anchorOrigin?: SnackbarOrigin;
autoHideDuration?: number;
open: boolean;
- message?: React.ReactElement<any>;
- kind: SnackbarKind;
- link?: string;
+ messages: SnackbarMessage[];
}
interface SnackbarEventProps {
- onClose?: (event: React.SyntheticEvent<any>, reason: string) => void;
+ onClose?: (event: React.SyntheticEvent<any>, reason: string, message?: string) => void;
onExited: () => void;
onClick: (uuid: string) => void;
}
return {
anchorOrigin: { vertical: "bottom", horizontal: "right" },
open: state.snackbar.open,
- message: <span>{messages.length > 0 ? messages[0].message : ""}</span>,
- autoHideDuration: messages.length > 0 ? messages[0].hideDuration : 0,
- kind: messages.length > 0 ? messages[0].kind : SnackbarKind.INFO,
- link: messages.length > 0 ? messages[0].link : ''
+ messages,
+ autoHideDuration: messages.length > 0 ? messages[0].hideDuration : 0
};
};
const mapDispatchToProps = (dispatch: Dispatch): SnackbarEventProps => ({
- onClose: (event: any, reason: string) => {
+ onClose: (event: any, reason: string, id: undefined) => {
if (reason !== "clickaway") {
- dispatch(snackbarActions.CLOSE_SNACKBAR());
+ dispatch(snackbarActions.CLOSE_SNACKBAR(id));
}
},
onExited: () => {
}
});
-type CssRules = "success" | "error" | "info" | "warning" | "icon" | "iconVariant" | "message" | "linkButton";
+type CssRules = "success" | "error" | "info" | "warning" | "icon" | "iconVariant" | "message" | "linkButton" | "snackbarContent";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
success: {
},
linkButton: {
fontWeight: 'bolder'
+ },
+ snackbarContent: {
+ marginBottom: '1rem'
}
});
[SnackbarKind.ERROR]: [ErrorIcon, classes.error]
};
- const [Icon, cssClass] = variants[props.kind];
-
-
-
return (
<MaterialSnackbar
open={props.open}
- message={props.message}
onClose={props.onClose}
onExited={props.onExited}
anchorOrigin={props.anchorOrigin}
autoHideDuration={props.autoHideDuration}>
- <div data-cy="snackbar"><SnackbarContent
- className={classNames(cssClass)}
- aria-describedby="client-snackbar"
- message={
- <span id="client-snackbar" className={classes.message}>
- <Icon className={classNames(classes.icon, classes.iconVariant)} />
- {props.message}
- </span>
+ <div data-cy="snackbar">
+ {
+ props.messages.map((message, index) => {
+ const [Icon, cssClass] = variants[message.kind];
+
+ return <SnackbarContent
+ key={`${index}-${message.message}`}
+ className={classNames(cssClass, classes.snackbarContent)}
+ aria-describedby="client-snackbar"
+ message={
+ <span id="client-snackbar" className={classes.message}>
+ <Icon className={classNames(classes.icon, classes.iconVariant)} />
+ {message.message}
+ </span>
+ }
+ action={actions(message, props.onClick, props.onClose, classes, index, props.autoHideDuration)}
+ />
+ })
}
- action={actions(props)}
- /></div>
+ </div>
</MaterialSnackbar>
);
}
));
-const actions = (props: SnackbarProps) => {
- const { link, onClose, onClick, classes } = props;
+const actions = (props: SnackbarMessage, onClick, onClose, classes, index, autoHideDuration) => {
+ if (onClose && autoHideDuration) {
+ setTimeout(onClose, autoHideDuration);
+ }
+
const actions = [
<IconButton
key="close"
aria-label="Close"
color="inherit"
- onClick={e => onClose && onClose(e, '')}>
+ onClick={e => onClose && onClose(e, '', index)}>
<CloseIcon className={classes.icon} />
</IconButton>
];
- if (link) {
+ if (props.link) {
actions.splice(0, 0,
<Button key="goTo"
aria-label="goTo"
size="small"
color="inherit"
className={classes.linkButton}
- onClick={() => onClick(link)}>
+ onClick={() => onClick(props.link)}>
<span data-cy='snackbar-goto-action'>Go To</span>
</Button>
);
import { getNodeChildrenIds, Tree as Ttree, createTree, getNode, TreeNodeStatus } from 'models/tree';
import { Dispatch } from "redux";
import { initTreeNode } from '../../models/tree';
+import { ResourcesState } from "store/resources/resources";
type Callback<T> = (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>, pickerId: string) => void;
export interface TreePickerProps<T> {
return item;
};
-const memoizedMapStateToProps = () => {
- let prevTree: Ttree<any>;
- let mappedProps: Pick<TreeProps<any>, 'items' | 'disableRipple' | 'itemsMap'>;
- return <T>(state: RootState, props: TreePickerProps<T>): Pick<TreeProps<T>, 'items' | 'disableRipple' | 'itemsMap'> => {
+const mapStateToProps =
+ <T>(state: RootState, props: TreePickerProps<T>): Pick<TreeProps<T>, 'items' | 'disableRipple' | 'itemsMap'> => {
const itemsIdMap: Map<string, TreeItem<T>> = new Map();
const tree = state.treePicker[props.pickerId] || createTree();
- if (tree !== prevTree) {
- prevTree = tree;
- mappedProps = {
- disableRipple: true,
- items: getNodeChildrenIds('')(tree)
- .map(treePickerToTreeItems(tree))
- .map(item => addToItemsIdMap(item, itemsIdMap))
- .map(parentItem => ({
- ...parentItem,
- flatTree: true,
- items: flatTree(itemsIdMap, 2, parentItem.items || []),
- })),
- itemsMap: itemsIdMap,
- };
- }
- return mappedProps;
+ return {
+ disableRipple: true,
+ items: getNodeChildrenIds('')(tree)
+ .map(treePickerToTreeItems(tree, state.resources))
+ .map(item => addToItemsIdMap(item, itemsIdMap))
+ .map(parentItem => ({
+ ...parentItem,
+ flatTree: true,
+ items: flatTree(itemsIdMap, 2, parentItem.items || []),
+ })),
+ itemsMap: itemsIdMap,
+ };
};
-};
const mapDispatchToProps = (_: Dispatch, props: TreePickerProps<any>): Pick<TreeProps<any>, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive' | 'toggleItemSelection'> => ({
onContextMenu: (event, item) => props.onContextMenu(event, item, props.pickerId),
toggleItemSelection: (event, item) => props.toggleItemSelection(event, item, props.pickerId),
});
-export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree);
+export const TreePicker = connect(mapStateToProps, mapDispatchToProps)(Tree);
-const treePickerToTreeItems = (tree: Ttree<any>) =>
+const treePickerToTreeItems = (tree: Ttree<any>, resources: ResourcesState) =>
(id: string): TreeItem<any> => {
const node = getNode(id)(tree) || initTreeNode({ id: '', value: 'InvalidNode' });
const items = getNodeChildrenIds(node.id)(tree)
- .map(treePickerToTreeItems(tree));
+ .map(treePickerToTreeItems(tree, resources));
+ const resource = resources[node.id];
return {
active: node.active,
- data: node.value,
+ data: resource ? { ...resource, name: node.value.name || node.value } : undefined || node.value,
id: node.id,
items: items.length > 0 ? items : undefined,
open: node.expanded,
import { FormDialog } from 'components/form-dialog/form-dialog';
import { VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, VIRTUAL_MACHINE_ADD_LOGIN_FORM, addUpdateVirtualMachineLogin, AddLoginFormData, VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD } from 'store/virtual-machines/virtual-machines-actions';
import { ParticipantSelect } from 'views-components/sharing-dialog/participant-select';
-import { GroupArrayInput } from 'views-components/virtual-machines-dialog/group-array-input';
+import { GroupArrayInput, GroupArrayDataProps } from 'views-components/virtual-machines-dialog/group-array-input';
export const VirtualMachineAddLoginDialog = compose(
withDialog(VIRTUAL_MACHINE_ADD_LOGIN_DIALOG),
}
})
)(
- (props: CreateGroupDialogComponentProps) =>
- <FormDialog
+ (props: CreateGroupDialogComponentProps) => {
+ const [hasPartialGroupInput, setPartialGroupInput] = React.useState<boolean>(false);
+
+ return <FormDialog
dialogTitle={props.data.updating ? "Update login permission" : "Add login permission"}
formFields={AddLoginFormFields}
submitLabel={props.data.updating ? "Update" : "Add"}
{...props}
- />
+ data={{
+ ...props.data,
+ setPartialGroupInput,
+ hasPartialGroupInput,
+ }}
+ invalid={props.invalid || hasPartialGroupInput}
+ />;
+ }
);
-type CreateGroupDialogComponentProps = WithDialogProps<{updating: boolean}> & InjectedFormProps<AddLoginFormData>;
+type CreateGroupDialogComponentProps = WithDialogProps<{updating: boolean}> & GroupArrayDataProps & InjectedFormProps<AddLoginFormData>;
const AddLoginFormFields = (props) => {
return <>
name={VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD}
input={{id:"Add groups to VM login (eg: docker, sudo)", disabled:false}}
required={false}
+ setPartialGroupInput={props.data.setPartialGroupInput}
+ hasPartialGroupInput={props.data.hasPartialGroupInput}
/>
</>;
}
import React from 'react';
import { StringArrayCommandInputParameter } from 'models/workflow';
-import { Field } from 'redux-form';
+import { Field, GenericField } from 'redux-form';
import { GenericInputProps } from 'views/run-process-panel/inputs/generic-input';
import { ChipsInput } from 'components/chips-input/chips-input';
import { identity } from 'lodash';
-import { withStyles, WithStyles, FormGroup, Input, InputLabel, FormControl } from '@material-ui/core';
+import { withStyles, WithStyles, FormGroup, Input, InputLabel, FormControl, FormHelperText } from '@material-ui/core';
+import classnames from "classnames";
+import { ArvadosTheme } from 'common/custom-theme';
-export interface StringArrayInputProps {
+export interface GroupArrayDataProps {
+ hasPartialGroupInput?: boolean;
+ setPartialGroupInput?: (value: boolean) => void;
+}
+
+interface GroupArrayFieldProps {
+ commandInput: StringArrayCommandInputParameter;
+}
+
+const GroupArrayField = Field as new () => GenericField<GroupArrayDataProps & GroupArrayFieldProps>;
+
+export interface GroupArrayInputProps {
name: string;
input: StringArrayCommandInputParameter;
required: boolean;
}
-type CssRules = 'chips';
+type CssRules = 'chips' | 'partialInputHelper' | 'partialInputHelperVisible';
-const styles = {
+const styles = (theme: ArvadosTheme) => ({
chips: {
marginTop: "16px",
},
-};
+ partialInputHelper: {
+ textAlign: 'right' as 'right',
+ visibility: 'hidden' as 'hidden',
+ color: theme.palette.error.dark,
+ },
+ partialInputHelperVisible: {
+ visibility: 'visible' as 'visible',
+ }
+});
-export const GroupArrayInput = ({name, input}: StringArrayInputProps) =>
- <Field
- name={name}
- commandInput={input}
- component={StringArrayInputComponent as any}
- />;
+export const GroupArrayInput = ({name, input, setPartialGroupInput, hasPartialGroupInput}: GroupArrayInputProps & GroupArrayDataProps) => {
+ console.log(hasPartialGroupInput);
+ return <GroupArrayField
+ name={name}
+ commandInput={input}
+ component={GroupArrayInputComponent as any}
+ setPartialGroupInput={setPartialGroupInput}
+ hasPartialGroupInput={hasPartialGroupInput}
+ />;
+}
-const StringArrayInputComponent = (props: GenericInputProps) => {
+const GroupArrayInputComponent = (props: GenericInputProps & GroupArrayDataProps) => {
return <FormGroup>
<FormControl fullWidth error={props.meta.error}>
<InputLabel shrink={props.meta.active || props.input.value.length > 0}>{props.commandInput.id}</InputLabel>
};
const StyledInputComponent = withStyles(styles)(
- class InputComponent extends React.PureComponent<GenericInputProps & WithStyles<CssRules>>{
+ class InputComponent extends React.PureComponent<GenericInputProps & WithStyles<CssRules> & GroupArrayDataProps>{
render() {
const { classes } = this.props;
- const { commandInput, input, meta } = this.props;
- return <ChipsInput
- deletable={!commandInput.disabled}
- orderable={!commandInput.disabled}
- disabled={commandInput.disabled}
- values={input.value}
- onChange={this.handleChange}
- handleFocus={input.onFocus}
- createNewValue={identity}
- inputComponent={Input}
- chipsClassName={classes.chips}
- pattern={/[_a-z][-0-9_a-z]*/ig}
- inputProps={{
- error: meta.error,
- }} />;
+ const { commandInput, input, meta, hasPartialGroupInput } = this.props;
+ return <>
+ <ChipsInput
+ deletable={!commandInput.disabled}
+ orderable={!commandInput.disabled}
+ disabled={commandInput.disabled}
+ values={input.value}
+ onChange={this.handleChange}
+ handleFocus={input.onFocus}
+ createNewValue={identity}
+ inputComponent={Input}
+ chipsClassName={classes.chips}
+ pattern={/[_a-z][-0-9_a-z]*/ig}
+ onPartialInput={this.props.setPartialGroupInput}
+ inputProps={{
+ error: meta.error || hasPartialGroupInput,
+ }} />
+ <FormHelperText className={classnames([classes.partialInputHelper, ...(hasPartialGroupInput ? [classes.partialInputHelperVisible] : [])])}>
+ Press enter to complete group name
+ </FormHelperText>
+ </>;
}
handleChange = (values: {}[]) => {
import { Link as ButtonLink } from '@material-ui/core';
import { ResourceWithName, ResponsiblePerson } from 'views-components/data-explorer/renderers';
import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
+import { resourceIsFrozen } from 'common/frozen-resources';
type CssRules = 'root'
| 'button'
}
}
}
+
+ if (item && isWritable) {
+ isWritable = !resourceIsFrozen(item, state.resources);
+ }
+
return { item, isWritable, isOldVersion };
})(
class extends React.Component<CollectionPanelProps> {
import { GroupContentsResource } from 'services/groups-service/groups-service';
import { GroupClass, GroupResource } from 'models/group';
import { CollectionResource } from 'models/collection';
+import { resourceIsFrozen } from 'common/frozen-resources';
type CssRules = 'root' | "button";
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
- const { resources } = this.props;
+ const { resources, isAdmin } = this.props;
const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
// When viewing the contents of a filter group, all contents should be treated as read only.
let readonly = false;
isTrashed: ('isTrashed' in resource) ? resource.isTrashed : false,
kind: resource.kind,
menuKind,
+ isAdmin,
+ isFrozen: resourceIsFrozen(resource, resources),
description: resource.description,
storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
properties: ('properties' in resource) ? resource.properties : {},
}
openDialog = () => {
+ this.componentDidMount();
this.setState({ open: true });
}
}
openDialog = () => {
+ this.componentDidMount();
this.setState({ open: true });
}
}
renderDialog() {
- return <Dialog
+ return this.state.open ? <Dialog
open={this.state.open}
onClose={this.closeDialog}
fullWidth
color='primary'
onClick={this.submit}>Ok</Button>
</DialogActions>
- </Dialog>;
+ </Dialog> : null;
}
});
import { openVirtualMachinesContextMenu } from 'store/context-menu/context-menu-actions';
import { ResourceUuid, VirtualMachineHostname, VirtualMachineLogin } from 'views-components/data-explorer/renderers';
-type CssRules = 'moreOptionsButton' | 'moreOptions' | 'chipsRoot';
+type CssRules = 'moreOptionsButton' | 'moreOptions' | 'chipsRoot' | 'vmTableWrapper';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
moreOptionsButton: {
chipsRoot: {
margin: `0px -${theme.spacing.unit / 2}px`,
},
+ vmTableWrapper: {
+ overflowX: 'auto',
+ },
});
const mapStateToProps = (state: RootState) => {
const CardContentWithVirtualMachines = (props: VirtualMachineProps) =>
<Grid item xs={12}>
<Card>
- <CardContent>
+ <CardContent className={props.classes.vmTableWrapper}>
{virtualMachinesTable(props)}
</CardContent>
</Card>
import CopyToClipboard from 'react-copy-to-clipboard';
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon' | 'chipsRoot' | 'copyIcon' | 'webshellButton';
+type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon' | 'chipsRoot' | 'copyIcon' | 'tableWrapper' | 'webshellButton';
const EXTRA_TOKEN = "exraToken";
fontSize: '1rem'
}
},
+ tableWrapper: {
+ overflowX: 'auto',
+ },
webshellButton: {
textTransform: "initial",
},
</Tooltip>
</a>
</div>
- {virtualMachinesTable(props)}
+ <div className={props.classes.tableWrapper}>
+ {virtualMachinesTable(props)}
+ </div>
</span>
</CardContent>
languageName: node
linkType: hard
-"caniuse-lite@npm:1.0.30001299, caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000981, caniuse-lite@npm:^1.0.30001035, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001219":
+"caniuse-lite@npm:1.0.30001299":
version: 1.0.30001299
resolution: "caniuse-lite@npm:1.0.30001299"
checksum: c770f60ebf3e0cc8043ba4db0ebec12d7a595a6b50cb4437c3c5c55b04de9d2413f711f2828be761e8c37bb46b927a8abe6b199b8f0ffc1a34af0ebdee84be27
languageName: node
linkType: hard
+"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000981, caniuse-lite@npm:^1.0.30001035, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001219":
+ version: 1.0.30001414
+ resolution: "caniuse-lite@npm:1.0.30001414"
+ checksum: 97210cfd15ded093b20c33d35bef9711a88402c3345411dad420c991a41a3e38ad17fd66721e8334c86e9b2e4aa2c1851d3631f1441afb73b92d93b2b8ca890d
+ languageName: node
+ linkType: hard
+
"capture-exit@npm:^2.0.0":
version: 2.0.0
resolution: "capture-exit@npm:2.0.0"