KEYCLOAK-14158 Polished the My Resource page
empty state change case added dropdown menu instead of buttons now on edit you can add and remove permissions changed how the actions work updated success messages use live region alerts toast alerts username or email search labels for the buttons margin between accecpt and deny button fixed test and types changed to bigger distance with split component changed to use seperate empty state component
This commit is contained in:
parent
33863ba161
commit
8b0760a6d1
15 changed files with 547 additions and 256 deletions
|
@ -19,6 +19,7 @@ package org.keycloak.services.resources.account.resources;
|
|||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
|
@ -205,7 +206,11 @@ public class ResourceService extends AbstractResourceService {
|
|||
UserModel user = users.getUserByUsername(requester, provider.getRealm());
|
||||
|
||||
if (user == null) {
|
||||
user = users.getUserById(requester, provider.getRealm());
|
||||
user = users.getUserByEmail(requester, provider.getRealm());
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
throw new NotFoundException("invalid_username_or_email");
|
||||
}
|
||||
|
||||
return user;
|
||||
|
|
|
@ -30,29 +30,27 @@ public class MyResourcesPage extends AbstractLoggedInPage {
|
|||
}
|
||||
|
||||
public void clickShareButton(int row) {
|
||||
final WebElement webElement = driver.findElement(By.id("ex-expand" + row));
|
||||
|
||||
//first button is share
|
||||
webElement.findElements(By.tagName("button")).get(0).click();
|
||||
driver.findElement(By.id("share-" + row)).click();
|
||||
}
|
||||
|
||||
public void clickEditButton(int row) {
|
||||
final WebElement webElement = driver.findElement(By.id("ex-expand" + row));
|
||||
|
||||
//first button share 2rd is the edit button
|
||||
webElement.findElements(By.tagName("button")).get(1).click();
|
||||
final WebElement webElement = driver.findElement(By.id("action-menu-" + row));
|
||||
webElement.click();
|
||||
webElement.findElement(By.id("edit-" + row)).click();
|
||||
}
|
||||
|
||||
public void createShare(String userName) {
|
||||
driver.findElement(By.id("username")).sendKeys(userName);
|
||||
driver.findElement(By.id("add")).click();
|
||||
driver.findElement(By.id("remove_pf-random-id-1")).click();
|
||||
driver.findElement(By.id("pf-toggle-id-6")).click();
|
||||
driver.findElement(By.id("Scope A-1")).click();
|
||||
driver.findElement(By.id("pf-toggle-id-9")).click();
|
||||
driver.findElement(By.id("done")).click();
|
||||
}
|
||||
|
||||
public void removeAllPermissions() {
|
||||
driver.findElement(By.id("remove_pf-random-id-1")).click();
|
||||
driver.findElement(By.id("remove_pf-random-id-2")).click();
|
||||
driver.findElement(By.className("pf-c-select__toggle-clear")).click();
|
||||
driver.findElement(By.id("save-0")).click();
|
||||
driver.findElement(By.id("done")).click();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,12 @@ firstPage=First Page
|
|||
resourceSharedWith=Resource is shared with {0}
|
||||
and=\ and {0} other users
|
||||
add=Add
|
||||
share=Share
|
||||
edit=Edit
|
||||
unShare=Unshare
|
||||
shareSuccess=Resource successfully shared.
|
||||
unShareSuccess=Resource successfully un-shared.
|
||||
updateSuccess=Resource successfully updated.
|
||||
resourceAlreadyShared=Resource is already shared with this user.
|
||||
resourceNotShared=This resource is not shared.
|
||||
permissionRequests=Permission Requests
|
||||
|
|
|
@ -15,16 +15,17 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Alert, AlertActionCloseButton} from '@patternfly/react-core';
|
||||
import {Msg} from '../widgets/Msg';
|
||||
import { Alert, AlertActionCloseButton, AlertGroup, AlertVariant } from '@patternfly/react-core';
|
||||
import { Msg } from '../widgets/Msg';
|
||||
|
||||
interface ContentAlertProps {}
|
||||
interface ContentAlertProps { }
|
||||
|
||||
type AlertVariant = 'success' | 'danger' | 'warning' | 'info';
|
||||
interface ContentAlertState {
|
||||
isVisible: boolean;
|
||||
message: string;
|
||||
variant: AlertVariant;
|
||||
alerts: {
|
||||
key: number;
|
||||
message: string;
|
||||
variant: AlertVariant;
|
||||
}[];
|
||||
}
|
||||
export class ContentAlert extends React.Component<ContentAlertProps, ContentAlertState> {
|
||||
private static instance: ContentAlert;
|
||||
|
@ -32,7 +33,9 @@ export class ContentAlert extends React.Component<ContentAlertProps, ContentAler
|
|||
private constructor(props: ContentAlertProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {isVisible: false, message: '', variant: 'success'};
|
||||
this.state = {
|
||||
alerts: []
|
||||
};
|
||||
ContentAlert.instance = this;
|
||||
}
|
||||
|
||||
|
@ -40,62 +43,69 @@ export class ContentAlert extends React.Component<ContentAlertProps, ContentAler
|
|||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static success(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert('success', message, params);
|
||||
ContentAlert.instance.postAlert(AlertVariant.success, message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static danger(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert('danger', message, params);
|
||||
ContentAlert.instance.postAlert(AlertVariant.danger, message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static warning(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert('warning', message, params);
|
||||
ContentAlert.instance.postAlert(AlertVariant.warning, message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static info(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert('info', message, params);
|
||||
ContentAlert.instance.postAlert(AlertVariant.info, message, params);
|
||||
}
|
||||
|
||||
private hideAlert = () => {
|
||||
this.setState({isVisible: false});
|
||||
private hideAlert = (key: number) => {
|
||||
this.setState({ alerts: [...this.state.alerts.filter(el => el.key !== key)] });
|
||||
}
|
||||
|
||||
private getUniqueId = () => (new Date().getTime());
|
||||
|
||||
private postAlert = (variant: AlertVariant, message: string, params?: string[]) => {
|
||||
this.setState({isVisible: true,
|
||||
message: Msg.localize(message, params),
|
||||
variant});
|
||||
const alerts = this.state.alerts;
|
||||
const key = this.getUniqueId();
|
||||
alerts.push({
|
||||
key,
|
||||
message: Msg.localize(message, params),
|
||||
variant
|
||||
});
|
||||
this.setState({ alerts });
|
||||
|
||||
if (variant !== 'danger') {
|
||||
setTimeout(() => this.setState({isVisible: false}), 5000);
|
||||
if (variant !== AlertVariant.danger) {
|
||||
setTimeout(() => this.hideAlert(key), 8000);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ this.state.isVisible &&
|
||||
<section className="pf-c-page__main-section pf-m-light">
|
||||
<AlertGroup isToast>
|
||||
{this.state.alerts.map(({ key, variant, message }) => (
|
||||
<Alert
|
||||
id="content-alert"
|
||||
title=''
|
||||
variant={this.state.variant}
|
||||
variantLabel=''
|
||||
aria-label=''
|
||||
action={<AlertActionCloseButton id="content-alert-close" onClose={this.hideAlert} />}
|
||||
>
|
||||
{this.state.message}
|
||||
</Alert>
|
||||
</section>
|
||||
}
|
||||
</React.Fragment>
|
||||
isLiveRegion
|
||||
variant={variant}
|
||||
title={message}
|
||||
action={
|
||||
<AlertActionCloseButton
|
||||
title={message}
|
||||
variantLabel={`${variant} alert`}
|
||||
onClose={() => this.hideAlert(key)}
|
||||
/>
|
||||
}
|
||||
key={key} />
|
||||
))}
|
||||
</AlertGroup>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,76 +19,76 @@ import * as React from 'react';
|
|||
import {
|
||||
Button,
|
||||
Modal,
|
||||
DataList,
|
||||
DataListItemRow,
|
||||
DataListItemCells,
|
||||
DataListCell,
|
||||
DataListItem,
|
||||
ChipGroup,
|
||||
ChipGroupToolbarItem,
|
||||
Chip
|
||||
Form,
|
||||
FormGroup,
|
||||
TextInput,
|
||||
InputGroup
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { EditAltIcon } from '@patternfly/react-icons';
|
||||
import { OkIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { Resource, Permission, Scope } from './MyResourcesPage';
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import AccountService, {HttpResponse} from '../../account-service/account.service';
|
||||
import AccountService from '../../account-service/account.service';
|
||||
import { ContentAlert } from '../ContentAlert';
|
||||
import { PermissionSelect } from './PermissionSelect';
|
||||
|
||||
interface EditTheResourceProps {
|
||||
resource: Resource;
|
||||
permissions: Permission[];
|
||||
onClose: (resource: Resource, row: number) => void;
|
||||
row: number;
|
||||
onClose: () => void;
|
||||
children: (toggle: () => void) => void;
|
||||
}
|
||||
|
||||
interface EditTheResourceState {
|
||||
changed: boolean[];
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export class EditTheResource extends React.Component<EditTheResourceProps, EditTheResourceState> {
|
||||
protected static defaultProps = { permissions: [], row: 0 };
|
||||
protected static defaultProps = { permissions: [] };
|
||||
|
||||
public constructor(props: EditTheResourceProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
changed: [],
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
private clearState(): void {
|
||||
this.setState({
|
||||
});
|
||||
this.setState({});
|
||||
}
|
||||
|
||||
private handleToggleDialog = () => {
|
||||
if (this.state.isOpen) {
|
||||
this.setState({ isOpen: false });
|
||||
this.props.onClose();
|
||||
} else {
|
||||
this.clearState();
|
||||
this.setState({ isOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
async deletePermission(permission: Permission, scope: Scope): Promise<void> {
|
||||
permission.scopes.splice(permission.scopes.indexOf(scope), 1);
|
||||
private updateChanged = (row: number) => {
|
||||
const changed = this.state.changed;
|
||||
changed[row] = !changed[row];
|
||||
this.setState({ changed });
|
||||
}
|
||||
|
||||
async savePermission(permission: Permission): Promise<void> {
|
||||
await AccountService.doPut(`/resources/${this.props.resource._id}/permissions`, [permission]);
|
||||
ContentAlert.success(Msg.localize('shareSuccess'));
|
||||
this.props.onClose(this.props.resource, this.props.row);
|
||||
ContentAlert.success(Msg.localize('updateSuccess'));
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button variant="link" onClick={this.handleToggleDialog}>
|
||||
<EditAltIcon /> Edit
|
||||
</Button>
|
||||
{this.props.children(this.handleToggleDialog)}
|
||||
|
||||
<Modal
|
||||
title={'Edit the resource - ' + this.props.resource.name}
|
||||
isLarge={true}
|
||||
isLarge
|
||||
isOpen={this.state.isOpen}
|
||||
onClose={this.handleToggleDialog}
|
||||
actions={[
|
||||
|
@ -97,48 +97,44 @@ export class EditTheResource extends React.Component<EditTheResourceProps, EditT
|
|||
</Button>,
|
||||
]}
|
||||
>
|
||||
<DataList aria-label={Msg.localize('resources')}>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key='resource-name-header' width={3}>
|
||||
<strong><Msg msgKey='User' /></strong>
|
||||
</DataListCell>,
|
||||
<DataListCell key='permissions-header' width={5}>
|
||||
<strong><Msg msgKey='permissions' /></strong>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
{this.props.permissions.map((p, row) => {
|
||||
return (
|
||||
<DataListItem key={'resource-' + row} aria-labelledby={p.username}>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key={'userName-' + row} width={5}>
|
||||
{p.username}
|
||||
</DataListCell>,
|
||||
<DataListCell key={'permission-' + row} width={5}>
|
||||
<ChipGroup withToolbar>
|
||||
<ChipGroupToolbarItem key='permissions' categoryName={Msg.localize('permissions')}>
|
||||
{
|
||||
p.scopes.length > 0 && p.scopes.map(scope => (
|
||||
<Chip key={scope.toString()} onClick={() => this.deletePermission(p, scope)}>
|
||||
{scope.displayName || scope}
|
||||
</Chip>
|
||||
))
|
||||
}
|
||||
</ChipGroupToolbarItem>
|
||||
</ChipGroup>
|
||||
</DataListCell>
|
||||
]}
|
||||
<Form isHorizontal>
|
||||
{this.props.permissions.map((p, row) => (
|
||||
<React.Fragment>
|
||||
<FormGroup
|
||||
fieldId={`username-${row}`}
|
||||
label={Msg.localize('User')}
|
||||
>
|
||||
<TextInput id={`username-${row}`} type="text" value={p.username} isDisabled />
|
||||
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
fieldId={`permissions-${row}`}
|
||||
label={Msg.localize('permissions')}
|
||||
isRequired
|
||||
>
|
||||
<InputGroup>
|
||||
<PermissionSelect
|
||||
scopes={this.props.resource.scopes}
|
||||
selected={(p.scopes as string[]).map(s => new Scope(s))}
|
||||
direction={row === this.props.permissions.length - 1 ? "up" : "down"}
|
||||
onSelect={selection => {
|
||||
p.scopes = selection.map(s => s.name);
|
||||
this.updateChanged(row);
|
||||
}}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
})}
|
||||
</DataList>
|
||||
<Button
|
||||
id={`save-${row}`}
|
||||
isDisabled={!this.state.changed[row]}
|
||||
onClick={() => this.savePermission(p)}
|
||||
>
|
||||
<OkIcon />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Form>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -75,7 +75,7 @@ export interface Permission {
|
|||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
scopes: Scope[]; // this should be Scope[] - fix API
|
||||
scopes: Scope[] | string[]; // this should be Scope[] - fix API
|
||||
username: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,21 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { Button, Modal, Text, Badge, DataListItem, DataList, TextVariants, DataListItemRow, DataListItemCells, DataListCell, Chip } from '@patternfly/react-core';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Text,
|
||||
Badge,
|
||||
DataListItem,
|
||||
DataList,
|
||||
TextVariants,
|
||||
DataListItemRow,
|
||||
DataListItemCells,
|
||||
DataListCell,
|
||||
Chip,
|
||||
Split,
|
||||
SplitItem
|
||||
} from '@patternfly/react-core';
|
||||
import { UserCheckIcon } from '@patternfly/react-icons';
|
||||
|
||||
import AccountService, {HttpResponse} from '../../account-service/account.service';
|
||||
|
@ -59,7 +73,7 @@ export class PermissionRequest extends React.Component<PermissionRequestProps, P
|
|||
const permissions: Permission[] = permissionsRequest.data || [];
|
||||
|
||||
// Erik - I had to add the exclamation point. Can we be sure that the 'find' will not return undefined?
|
||||
const userScopes = permissions.find((p: Permission) => p.username === username)!.scopes;
|
||||
const userScopes = permissions.find((p: Permission) => p.username === username)!.scopes as Scope[];
|
||||
if (approve) {
|
||||
userScopes.push(...scopes);
|
||||
}
|
||||
|
@ -124,15 +138,22 @@ export class PermissionRequest extends React.Component<PermissionRequestProps, P
|
|||
<Text component={TextVariants.small}>{shareRequest.email}</Text>
|
||||
</DataListCell>,
|
||||
<DataListCell key={`permissions${i}`}>
|
||||
{shareRequest.scopes.map((scope, j) => <Chip key={j} isReadOnly>{scope}</Chip>)}
|
||||
{(shareRequest.scopes as Scope[]).map((scope, j) => <Chip key={j} isReadOnly>{scope}</Chip>)}
|
||||
</DataListCell>,
|
||||
<DataListCell key={`actions${i}`}>
|
||||
<Button onClick={() => this.handleApprove(shareRequest.username, shareRequest.scopes)}>
|
||||
Accept
|
||||
</Button>
|
||||
<Button variant="danger" onClick={() => this.handleDeny(shareRequest.username, shareRequest.scopes)}>
|
||||
Deny
|
||||
</Button>
|
||||
<Split gutter="sm">
|
||||
<SplitItem>
|
||||
<Button
|
||||
onClick={() => this.handleApprove(shareRequest.username, shareRequest.scopes as Scope[])}>
|
||||
Accept
|
||||
</Button>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Button variant="danger" onClick={() => this.handleDeny(shareRequest.username, shareRequest.scopes as Scope[])}>
|
||||
Deny
|
||||
</Button>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</DataListCell>
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { Select, SelectOption, SelectVariant, SelectOptionObject } from '@patternfly/react-core';
|
||||
import { Scope } from './MyResourcesPage';
|
||||
|
||||
interface PermissionSelectState {
|
||||
selected: ScopeValue[];
|
||||
isExpanded: boolean;
|
||||
scopes: JSX.Element[];
|
||||
}
|
||||
|
||||
interface PermissionSelectProps {
|
||||
scopes: Scope[];
|
||||
selected?: Scope[];
|
||||
direction?: 'up' | 'down';
|
||||
onSelect: (selected: Scope[]) => void;
|
||||
}
|
||||
|
||||
class ScopeValue implements SelectOptionObject {
|
||||
value: Scope;
|
||||
constructor(value: Scope) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.value.displayName ? this.value.displayName : this.value.name;
|
||||
}
|
||||
|
||||
compareTo(selectOption: Scope): boolean {
|
||||
return selectOption.name === this.value.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class PermissionSelect extends React.Component<PermissionSelectProps, PermissionSelectState> {
|
||||
constructor(props: PermissionSelectProps) {
|
||||
super(props);
|
||||
|
||||
let values: ScopeValue[] = [];
|
||||
if (this.props.selected) {
|
||||
values = this.props.selected!.map(s => new ScopeValue(s))
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isExpanded: false,
|
||||
selected: values,
|
||||
scopes: this.props.scopes.map((option, index) => (
|
||||
<SelectOption key={index} value={values.find(s => s.compareTo(option)) || new ScopeValue(option)} />
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
private onSelect = (_event: React.MouseEvent | React.ChangeEvent, selection: ScopeValue): void => {
|
||||
const { selected } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
if (selected.includes(selection)) {
|
||||
this.setState(
|
||||
prevState => ({ selected: prevState.selected.filter(item => item !== selection) }),
|
||||
() => onSelect(this.state.selected.map(sv => sv.value))
|
||||
);
|
||||
} else {
|
||||
this.setState(
|
||||
prevState => ({ selected: [...prevState.selected, selection] }),
|
||||
() => onSelect(this.state.selected.map(sv => sv.value))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private onToggle = (isExpanded: boolean) => {
|
||||
this.setState({
|
||||
isExpanded
|
||||
});
|
||||
}
|
||||
|
||||
private clearSelection = () => {
|
||||
this.setState({
|
||||
selected: [],
|
||||
isExpanded: false
|
||||
});
|
||||
this.props.onSelect([]);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isExpanded, selected } = this.state;
|
||||
const titleId = 'permission-id';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span id={titleId} hidden>
|
||||
Select the permissions
|
||||
</span>
|
||||
<Select
|
||||
direction={this.props.direction || 'down'}
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
ariaLabelTypeAhead="Select the permissions"
|
||||
onToggle={this.onToggle}
|
||||
onSelect={this.onSelect}
|
||||
onClear={this.clearSelection}
|
||||
selections={selected}
|
||||
isExpanded={isExpanded}
|
||||
ariaLabelledBy={titleId}
|
||||
placeholderText="Select the permissions"
|
||||
>
|
||||
{this.state.scopes}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,24 +26,32 @@ import {
|
|||
DataListItemCells,
|
||||
Level,
|
||||
LevelItem,
|
||||
Stack,
|
||||
StackItem,
|
||||
Button
|
||||
} from '@patternfly/react-core';
|
||||
Button,
|
||||
DataListAction,
|
||||
DataListActionVisibility,
|
||||
Dropdown,
|
||||
DropdownPosition,
|
||||
DropdownItem,
|
||||
KebabToggle
|
||||
} from '@patternfly/react-core';
|
||||
import { css } from '@patternfly/react-styles';
|
||||
|
||||
import { Remove2Icon } from '@patternfly/react-icons';
|
||||
import { Remove2Icon, RepositoryIcon, ShareAltIcon, EditAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import AccountService, {HttpResponse} from '../../account-service/account.service';
|
||||
import {PermissionRequest} from "./PermissionRequest";
|
||||
import {ShareTheResource} from "./ShareTheResource";
|
||||
import {Permission, Resource} from "./MyResourcesPage";
|
||||
import AccountService, { HttpResponse } from '../../account-service/account.service';
|
||||
import { PermissionRequest } from "./PermissionRequest";
|
||||
import { ShareTheResource } from "./ShareTheResource";
|
||||
import { Permission, Resource } from "./MyResourcesPage";
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import { ResourcesTableState, ResourcesTableProps, AbstractResourcesTable } from './AbstractResourceTable';
|
||||
import { EditTheResource } from './EditTheResource';
|
||||
import { ContentAlert } from '../ContentAlert';
|
||||
import EmptyMessageState from '../../widgets/EmptyMessageState';
|
||||
|
||||
export interface CollapsibleResourcesTableState extends ResourcesTableState {
|
||||
isRowOpen: boolean[];
|
||||
contextOpen: boolean[];
|
||||
isModalActive: boolean;
|
||||
}
|
||||
|
||||
export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesTableState> {
|
||||
|
@ -51,7 +59,9 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
|
|||
public constructor(props: ResourcesTableProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isRowOpen: new Array<boolean>(props.resources.data.length).fill(false),
|
||||
isRowOpen: [],
|
||||
contextOpen: [],
|
||||
isModalActive: false,
|
||||
permissions: new Map()
|
||||
}
|
||||
}
|
||||
|
@ -60,35 +70,50 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
|
|||
const newIsRowOpen: boolean[] = this.state.isRowOpen;
|
||||
newIsRowOpen[row] = !newIsRowOpen[row];
|
||||
if (newIsRowOpen[row]) this.fetchPermissions(this.props.resources.data[row], row);
|
||||
this.setState({isRowOpen: newIsRowOpen});
|
||||
this.setState({ isRowOpen: newIsRowOpen });
|
||||
};
|
||||
|
||||
private onContextToggle = (row: number, isOpen: boolean): void => {
|
||||
if (this.state.isModalActive) return;
|
||||
const data = this.props.resources.data;
|
||||
const contextOpen = this.state.contextOpen;
|
||||
contextOpen[row] = isOpen;
|
||||
if (isOpen) {
|
||||
const index = row > data.length ? row - data.length - 1 : row;
|
||||
this.fetchPermissions(data[index], index);
|
||||
}
|
||||
this.setState({ contextOpen });
|
||||
}
|
||||
|
||||
private fetchPermissions(resource: Resource, row: number): void {
|
||||
AccountService.doGet(`/resources/${resource._id}/permissions`)
|
||||
.then((response: HttpResponse<Permission[]>) => {
|
||||
const newPermissions: Map<number, Permission[]> = new Map(this.state.permissions);
|
||||
newPermissions.set(row, response.data || []);
|
||||
this.setState({permissions: newPermissions});
|
||||
}
|
||||
);
|
||||
.then((response: HttpResponse<Permission[]>) => {
|
||||
const newPermissions: Map<number, Permission[]> = new Map(this.state.permissions);
|
||||
newPermissions.set(row, response.data || []);
|
||||
this.setState({ permissions: newPermissions });
|
||||
});
|
||||
}
|
||||
|
||||
private removeShare(resource: Resource, row: number): void {
|
||||
private removeShare(resource: Resource, row: number): Promise<void> {
|
||||
const permissions = this.state.permissions.get(row)!.map(a => ({ username: a.username, scopes: [] }));
|
||||
AccountService.doPut(`/resources/${resource._id}/permissions`, permissions)
|
||||
return AccountService.doPut(`/resources/${resource._id}/permissions`, permissions)
|
||||
.then(() => {
|
||||
ContentAlert.success(Msg.localize('shareSuccess'));
|
||||
this.onToggle(row);
|
||||
ContentAlert.success(Msg.localize('unShareSuccess'));
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.props.resources.data.length === 0) {
|
||||
return (
|
||||
<EmptyMessageState icon={RepositoryIcon} messageKey="notHaveAnyResource"/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DataList aria-label={Msg.localize('resources')} id="resourcesList">
|
||||
<DataListItem key='resource-header' aria-labelledby='resource-header'>
|
||||
<DataListItemRow>
|
||||
// invisible toggle allows headings to line up properly
|
||||
<span style={{visibility: 'hidden'}}>
|
||||
<span style={{ visibility: 'hidden' }}>
|
||||
<DataListToggle
|
||||
isExpanded={false}
|
||||
id='resource-header-invisible-toggle'
|
||||
|
@ -98,25 +123,23 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
|
|||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key='resource-name-header' width={5}>
|
||||
<strong><Msg msgKey='resourceName'/></strong>
|
||||
<strong><Msg msgKey='resourceName' /></strong>
|
||||
</DataListCell>,
|
||||
<DataListCell key='application-name-header' width={5}>
|
||||
<strong><Msg msgKey='application'/></strong>
|
||||
<strong><Msg msgKey='application' /></strong>
|
||||
</DataListCell>,
|
||||
<DataListCell key='permission-request-header' width={5}>
|
||||
<strong><Msg msgKey='permissionRequests'/></strong>
|
||||
<strong><Msg msgKey='permissionRequests' /></strong>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
{(this.props.resources.data.length === 0) && <Msg msgKey="notHaveAnyResource"/>}
|
||||
{this.props.resources.data.map((resource: Resource, row: number) => {
|
||||
return (
|
||||
{this.props.resources.data.map((resource: Resource, row: number) => (
|
||||
<DataListItem key={'resource-' + row} aria-labelledby={resource.name} isExpanded={this.state.isRowOpen[row]}>
|
||||
<DataListItemRow>
|
||||
<DataListToggle
|
||||
onClick={()=>this.onToggle(row)}
|
||||
onClick={() => this.onToggle(row)}
|
||||
isExpanded={this.state.isRowOpen[row]}
|
||||
id={'resourceToggle-' + row}
|
||||
aria-controls="ex-expand1"
|
||||
|
@ -124,7 +147,7 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
|
|||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key={'resourceName-' + row} width={5}>
|
||||
<Msg msgKey={resource.name}/>
|
||||
<Msg msgKey={resource.name} />
|
||||
</DataListCell>,
|
||||
<DataListCell key={'resourceClient-' + row} width={5}>
|
||||
<a href={resource.client.baseUrl}>{this.getClientName(resource.client)}</a>
|
||||
|
@ -139,6 +162,143 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
|
|||
</DataListCell>
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
className={DataListActionVisibility.hiddenOnXl}
|
||||
aria-labelledby="check-action-item3 check-action-action3"
|
||||
id="check-action-action3"
|
||||
aria-label="Actions"
|
||||
>
|
||||
<Dropdown
|
||||
isPlain
|
||||
position={DropdownPosition.right}
|
||||
onSelect={() => this.setState({ isModalActive: true })}
|
||||
toggle={<KebabToggle onToggle={isOpen => this.onContextToggle(row + this.props.resources.data.length + 1, isOpen)} />}
|
||||
isOpen={this.state.contextOpen[row + this.props.resources.data.length + 1]}
|
||||
dropdownItems={[
|
||||
<ShareTheResource
|
||||
resource={resource}
|
||||
permissions={this.state.permissions.get(row)!}
|
||||
sharedWithUsersMsg={this.sharedWithUsersMessage(row)}
|
||||
onClose={() => {
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row + this.props.resources.data.length + 1, false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{
|
||||
(toggle: () => void) => (
|
||||
<DropdownItem id={'mob-share-' + row} key="mob-share" onClick={toggle}>
|
||||
<ShareAltIcon /> <Msg msgKey="share"/>
|
||||
</DropdownItem>)
|
||||
}
|
||||
</ShareTheResource>,
|
||||
<EditTheResource
|
||||
resource={resource}
|
||||
permissions={this.state.permissions.get(row)!}
|
||||
onClose={() => {
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row + this.props.resources.data.length + 1, false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{
|
||||
(toggle: () => void) => (
|
||||
<DropdownItem
|
||||
id={'mob-edit-' + row} key="mob-edit"
|
||||
isDisabled={this.numOthers(row) < 0}
|
||||
onClick={toggle}
|
||||
>
|
||||
<EditAltIcon /> <Msg msgKey="edit"/>
|
||||
</DropdownItem>)
|
||||
}
|
||||
</EditTheResource>,
|
||||
<DropdownItem
|
||||
id={'mob-remove-' + row}
|
||||
key="mob-remove"
|
||||
isDisabled={this.numOthers(row) < 0}
|
||||
onClick={() => {
|
||||
this.removeShare(resource, row).then(() =>
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row + this.props.resources.data.length + 1, false);
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Remove2Icon /> <Msg msgKey="unShare"/>
|
||||
</DropdownItem>
|
||||
]}
|
||||
/>
|
||||
</DataListAction>
|
||||
<DataListAction
|
||||
id={`actions-${row}`}
|
||||
className={css(DataListActionVisibility.visibleOnXl, DataListActionVisibility.hidden)}
|
||||
aria-labelledby="Row actions"
|
||||
aria-label="Actions"
|
||||
>
|
||||
<ShareTheResource
|
||||
resource={resource}
|
||||
permissions={this.state.permissions.get(row)!}
|
||||
sharedWithUsersMsg={this.sharedWithUsersMessage(row)}
|
||||
onClose={() => this.fetchPermissions(resource, row)}
|
||||
>
|
||||
{
|
||||
(toggle: () => void) => (
|
||||
<Button id={`share-${row}`} variant="link" onClick={toggle}>
|
||||
<ShareAltIcon /> <Msg msgKey="share"/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</ShareTheResource>
|
||||
<Dropdown
|
||||
id={`action-menu-${row}`}
|
||||
isPlain
|
||||
position={DropdownPosition.right}
|
||||
toggle={<KebabToggle onToggle={isOpen => this.onContextToggle(row, isOpen)} />}
|
||||
onSelect={() => this.setState({ isModalActive: true })}
|
||||
isOpen={this.state.contextOpen[row]}
|
||||
dropdownItems={[
|
||||
<EditTheResource
|
||||
resource={resource}
|
||||
permissions={this.state.permissions.get(row)!}
|
||||
onClose={() => {
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row, false);
|
||||
this.fetchPermissions(resource, row);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{
|
||||
(toggle: () => void) => (
|
||||
<DropdownItem
|
||||
id={'edit-' + row}
|
||||
key="edit"
|
||||
component="button"
|
||||
isDisabled={this.numOthers(row) < 0}
|
||||
onClick={toggle}
|
||||
>
|
||||
<EditAltIcon /> <Msg msgKey="edit"/>
|
||||
</DropdownItem>)
|
||||
}
|
||||
</EditTheResource>,
|
||||
<DropdownItem
|
||||
id={'remove-' + row}
|
||||
key="remove"
|
||||
component="button"
|
||||
isDisabled={this.numOthers(row) < 0}
|
||||
onClick={() => {
|
||||
this.removeShare(resource, row).then(() =>
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row, false);
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Remove2Icon /> <Msg msgKey="unShare"/>
|
||||
</DropdownItem>
|
||||
]}
|
||||
/>
|
||||
</DataListAction>
|
||||
|
||||
</DataListItemRow>
|
||||
<DataListContent
|
||||
noPadding={false}
|
||||
|
@ -146,43 +306,14 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
|
|||
id={'ex-expand' + row}
|
||||
isHidden={!this.state.isRowOpen[row]}
|
||||
>
|
||||
<Stack gutter='md'>
|
||||
<StackItem isFilled>
|
||||
<Level gutter='md'>
|
||||
<LevelItem><span/></LevelItem>
|
||||
<LevelItem id={'shared-with-user-message-' + row}>{this.sharedWithUsersMessage(row)}</LevelItem>
|
||||
<LevelItem><span/></LevelItem>
|
||||
</Level>
|
||||
</StackItem>
|
||||
<StackItem isFilled>
|
||||
<Level gutter='md'>
|
||||
<LevelItem><span/></LevelItem>
|
||||
<LevelItem>
|
||||
<ShareTheResource resource={resource}
|
||||
permissions={this.state.permissions.get(row)!}
|
||||
sharedWithUsersMsg={this.sharedWithUsersMessage(row)}
|
||||
onClose={this.fetchPermissions.bind(this)}
|
||||
row={row}/>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<EditTheResource resource={resource} permissions={this.state.permissions.get(row)!} row={row} onClose={this.fetchPermissions.bind(this)}/>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<Button
|
||||
isDisabled={this.numOthers(row) < 0}
|
||||
variant="link"
|
||||
onClick={() => this.removeShare(resource, row)}
|
||||
>
|
||||
<Remove2Icon/> Remove
|
||||
</Button>
|
||||
</LevelItem>
|
||||
<LevelItem><span/></LevelItem>
|
||||
</Level>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Level gutter='md'>
|
||||
<LevelItem><span /></LevelItem>
|
||||
<LevelItem id={'shared-with-user-message-' + row}>{this.sharedWithUsersMessage(row)}</LevelItem>
|
||||
<LevelItem><span /></LevelItem>
|
||||
</Level>
|
||||
</DataListContent>
|
||||
</DataListItem>
|
||||
)})}
|
||||
))}
|
||||
</DataList>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,19 +31,18 @@ import {
|
|||
TextInput
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { ShareAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import AccountService, {HttpResponse} from '../../account-service/account.service';
|
||||
import AccountService from '../../account-service/account.service';
|
||||
import { Resource, Permission, Scope } from './MyResourcesPage';
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import {ContentAlert} from '../ContentAlert';
|
||||
import { PermissionSelect } from './PermissionSelect';
|
||||
|
||||
interface ShareTheResourceProps {
|
||||
resource: Resource;
|
||||
permissions: Permission[];
|
||||
sharedWithUsersMsg: React.ReactNode;
|
||||
onClose: (resource: Resource, row: number) => void;
|
||||
row: number;
|
||||
onClose: () => void;
|
||||
children: (toggle: () => void) => void;
|
||||
}
|
||||
|
||||
interface ShareTheResourceState {
|
||||
|
@ -58,7 +57,7 @@ interface ShareTheResourceState {
|
|||
* @author Stan Silvert ssilvert@redhat.com (C) 2019 Red Hat Inc.
|
||||
*/
|
||||
export class ShareTheResource extends React.Component<ShareTheResourceProps, ShareTheResourceState> {
|
||||
protected static defaultProps = {permissions: [], row: 0};
|
||||
protected static defaultProps = {permissions: []};
|
||||
|
||||
public constructor(props: ShareTheResourceProps) {
|
||||
super(props);
|
||||
|
@ -100,13 +99,14 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
|
|||
AccountService.doPut(`/resources/${rscId}/permissions`, permissions)
|
||||
.then(() => {
|
||||
ContentAlert.success('shareSuccess');
|
||||
this.props.onClose(this.props.resource, this.props.row);
|
||||
this.props.onClose();
|
||||
})
|
||||
};
|
||||
|
||||
private handleToggleDialog = () => {
|
||||
if (this.state.isOpen) {
|
||||
this.setState({isOpen: false});
|
||||
this.props.onClose();
|
||||
} else {
|
||||
this.clearState();
|
||||
this.setState({isOpen: true});
|
||||
|
@ -136,21 +136,6 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
|
|||
this.setState({usernames: newUsernames});
|
||||
}
|
||||
|
||||
private handleSelectPermission = (selectedPermission: Scope) => {
|
||||
let newPermissionsSelected: Scope[] = this.state.permissionsSelected;
|
||||
let newPermissionsUnSelected: Scope[] = this.state.permissionsUnSelected;
|
||||
|
||||
if (newPermissionsSelected.includes(selectedPermission)) {
|
||||
newPermissionsSelected = newPermissionsSelected.filter(permission => permission !== selectedPermission);
|
||||
newPermissionsUnSelected.push(selectedPermission);
|
||||
} else {
|
||||
newPermissionsUnSelected = newPermissionsUnSelected.filter(permission => permission !== selectedPermission);
|
||||
newPermissionsSelected.push(selectedPermission);
|
||||
}
|
||||
|
||||
this.setState({permissionsSelected: newPermissionsSelected, permissionsUnSelected: newPermissionsUnSelected});
|
||||
}
|
||||
|
||||
private isAddDisabled(): boolean {
|
||||
return this.state.usernameInput === '' || this.isAlreadyShared();
|
||||
}
|
||||
|
@ -168,12 +153,9 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
|
|||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button variant="link" onClick={this.handleToggleDialog}>
|
||||
<ShareAltIcon/> Share
|
||||
</Button>
|
||||
{this.props.children(this.handleToggleDialog)}
|
||||
|
||||
<Modal
|
||||
title={'Share the resource - ' + this.props.resource.name}
|
||||
|
@ -207,7 +189,7 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
|
|||
isValid={!this.isAlreadyShared()}
|
||||
id="username"
|
||||
aria-describedby="username-helper"
|
||||
placeholder="username or email"
|
||||
placeholder="Username or email"
|
||||
onChange={this.handleUsernameChange}
|
||||
onKeyPress={this.handleEnterKeyInAddField}
|
||||
/>
|
||||
|
@ -233,30 +215,11 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
|
|||
label=""
|
||||
fieldId="permissions-selected"
|
||||
>
|
||||
{this.state.permissionsSelected.length < 1 && <strong>Select permissions below:</strong>}
|
||||
<ChipGroup withToolbar>
|
||||
<ChipGroupToolbarItem key='permissions-selected' categoryName='Grant Permissions '>
|
||||
{this.state.permissionsSelected.map((currentChip: Scope) => (
|
||||
<Chip key={currentChip.toString()} onClick={() => this.handleSelectPermission(currentChip)}>
|
||||
{currentChip.toString()}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroupToolbarItem>
|
||||
</ChipGroup>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label=""
|
||||
fieldId="permissions-not-selected"
|
||||
>
|
||||
<ChipGroup withToolbar>
|
||||
<ChipGroupToolbarItem key='permissions-unselected' categoryName='Not Selected '>
|
||||
{this.state.permissionsUnSelected.map((currentChip: Scope) => (
|
||||
<Chip key={currentChip.toString()} onClick={() => this.handleSelectPermission(currentChip)}>
|
||||
{currentChip.toString()}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroupToolbarItem>
|
||||
</ChipGroup>
|
||||
<PermissionSelect
|
||||
scopes={this.state.permissionsUnSelected}
|
||||
onSelect={selection => this.setState({ permissionsSelected: selection })}
|
||||
direction="up"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</StackItem>
|
||||
|
|
|
@ -26,11 +26,13 @@ import {
|
|||
ChipGroupToolbarItem,
|
||||
Chip
|
||||
} from '@patternfly/react-core';
|
||||
import { RepositoryIcon } from '@patternfly/react-icons';
|
||||
|
||||
|
||||
import {PaginatedResources, Resource} from "./MyResourcesPage";
|
||||
import {PaginatedResources, Resource, Scope} from "./MyResourcesPage";
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import { AbstractResourcesTable, ResourcesTableState } from './AbstractResourceTable';
|
||||
import EmptyMessageState from '../../widgets/EmptyMessageState';
|
||||
|
||||
export interface ResourcesTableProps {
|
||||
resources: PaginatedResources;
|
||||
|
@ -47,6 +49,11 @@ export class SharedResourcesTable extends AbstractResourcesTable<ResourcesTableS
|
|||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.props.resources.data.length === 0) {
|
||||
return (
|
||||
<EmptyMessageState icon={RepositoryIcon} messageKey="noResourcesSharedWithYou"/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DataList aria-label={Msg.localize('resources')}>
|
||||
<DataListItem key='resource-header' aria-labelledby='resource-header'>
|
||||
|
@ -65,7 +72,6 @@ export class SharedResourcesTable extends AbstractResourcesTable<ResourcesTableS
|
|||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
{(this.props.resources.data.length === 0) && <Msg msgKey="noResourcesSharedWithYou"/>}
|
||||
{this.props.resources.data.map((resource: Resource, row: number) => (
|
||||
<DataListItem key={'resource-' + row} aria-labelledby={resource.name}>
|
||||
<DataListItemRow>
|
||||
|
@ -96,7 +102,7 @@ export class SharedResourcesTable extends AbstractResourcesTable<ResourcesTableS
|
|||
<ChipGroup withToolbar>
|
||||
<ChipGroupToolbarItem key='permissions' categoryName={Msg.localize('pending')}>
|
||||
{
|
||||
resource.shareRequests[0].scopes.map(scope => (
|
||||
(resource.shareRequests[0].scopes as Scope[]).map(scope => (
|
||||
<Chip key={scope.name} isReadOnly>
|
||||
{scope.displayName || scope.name}
|
||||
</Chip>
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
import * as React from 'react';
|
||||
|
||||
import {EmptyState, EmptyStateBody, EmptyStateIcon, Title, TitleLevel} from '@patternfly/react-core';
|
||||
import { WarningTriangleIcon } from '@patternfly/react-icons';
|
||||
import {withRouter, RouteComponentProps} from 'react-router-dom';
|
||||
import {Msg} from '../../widgets/Msg';
|
||||
import EmptyMessageState from '../../widgets/EmptyMessageState';
|
||||
|
||||
export interface PageNotFoundProps extends RouteComponentProps {}
|
||||
|
||||
|
@ -21,15 +21,9 @@ class PgNotFound extends React.Component<PageNotFoundProps> {
|
|||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<EmptyState variant='full'>
|
||||
<EmptyStateIcon icon={WarningTriangleIcon} />
|
||||
<Title headingLevel={TitleLevel.h5} size="lg">
|
||||
<Msg msgKey='pageNotFound'/>
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
<Msg msgKey='invalidRoute' params={[this.props.location.pathname]} />
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
<EmptyMessageState icon={WarningTriangleIcon} messageKey="pageNotFound">
|
||||
<Msg msgKey="invalidRoute" params={[this.props.location.pathname]} />
|
||||
</EmptyMessageState>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateVariant,
|
||||
Title,
|
||||
EmptyStateIcon,
|
||||
TitleLevel,
|
||||
EmptyStateBody,
|
||||
IconProps,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
import { Msg } from './Msg';
|
||||
|
||||
export interface EmptyMessageStateProps {
|
||||
icon: React.FunctionComponent<IconProps>;
|
||||
messageKey: string;
|
||||
}
|
||||
|
||||
export default class EmptyMessageState extends React.Component<EmptyMessageStateProps, {}> {
|
||||
constructor(props: EmptyMessageStateProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EmptyState variant={EmptyStateVariant.full}>
|
||||
<EmptyStateIcon icon={this.props.icon} />
|
||||
<Title headingLevel={TitleLevel.h5} size="lg">
|
||||
<Msg msgKey={this.props.messageKey} />
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{this.props.children}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2582,9 +2582,9 @@
|
|||
}
|
||||
},
|
||||
"@patternfly/react-styles": {
|
||||
"version": "3.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-3.7.12.tgz",
|
||||
"integrity": "sha512-vTKyC78oKlrS6VTQ3GPYevc17qgxj2Ono+SCDwoMyhUexPEyXRuZHLoZA1/MkJHvSCqJHGBageBAFcRq5wb0XQ==",
|
||||
"version": "3.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-3.7.14.tgz",
|
||||
"integrity": "sha512-NVwbPP9JroulfQgj0LOLWKP4DumArW8RrP1FB1lLOCuw13KkuAcFbLN9MSF8ZBwJ8syxGEdux5mDC3jPjsrQiw==",
|
||||
"requires": {
|
||||
"camel-case": "^3.0.0",
|
||||
"css": "^2.2.3",
|
||||
|
@ -3211,9 +3211,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"resolve": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
|
||||
"integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
|
||||
"integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
|
||||
"requires": {
|
||||
"path-parse": "^1.0.6"
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"dependencies": {
|
||||
"@patternfly/react-core": "^3.153.3",
|
||||
"@patternfly/react-icons": "^3.15.16",
|
||||
"@patternfly/react-styles": "^3.7.14",
|
||||
"react": "npm:@pika/react@^16.13.1",
|
||||
"react-dom": "npm:@pika/react-dom@^16.13.1",
|
||||
"react-router-dom": "^4.3.1"
|
||||
|
|
Loading…
Reference in a new issue