KEYCLOAK-13121 added the basic functionality

This commit is contained in:
Erik Jan de Wit 2020-03-03 09:50:08 +01:00 committed by Stan Silvert
parent da1138a8d2
commit 7580be8708
14 changed files with 724 additions and 74 deletions

View file

@ -25,11 +25,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -111,6 +107,30 @@ public class ResourcesService extends AbstractResourceService {
.stream(), first, max);
}
/**
*/
@GET
@Path("pending-requests")
@Produces(MediaType.APPLICATION_JSON)
public Response getPendingRequests() {
Map<String, String> filters = new HashMap<>();
filters.put(PermissionTicket.REQUESTER, user.getId());
filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString());
final List<PermissionTicket> permissionTickets = ticketStore.find(filters, null, -1, -1);
final List<ResourcePermission> resourceList = new ArrayList<>(permissionTickets.size());
for (PermissionTicket ticket : permissionTickets) {
ResourcePermission resourcePermission = new ResourcePermission(ticket.getResource(), provider);
resourcePermission.addScope(new Scope(ticket.getScope()));
resourceList.add(resourcePermission);
}
return queryResponse(
(f, m) -> resourceList.stream(), -1, resourceList.size());
}
@Path("{id}")
public Object getResource(@PathParam("id") String id) {
org.keycloak.authorization.model.Resource resource = resourceStore.findById(id, null);
@ -203,7 +223,7 @@ public class ResourcesService extends AbstractResourceService {
if (first > 0) {
links.add(Link.fromUri(
KeycloakUriBuilder.fromUri(request.getUri().getRequestUri()).replaceQuery("first={first}&max={max}")
.build(nextPage ? first : first - max, max))
.build(Math.max(first - max, 0), max))
.rel("prev").build());
}

View file

@ -178,7 +178,11 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest {
assertEquals(10, resources.size());
resources = getMyResources(20, 10, response -> {
assertNextPageLink(response, "/realms/test/account/resources", 20, 10, true);
assertNextPageLink(response, "/realms/test/account/resources", 30, 10, true);
});
getMyResources(15, 5, response -> {
assertNextPageLink(response, "/realms/test/account/resources", 20, 5);
});
assertEquals(10, resources.size());
@ -768,7 +772,7 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest {
if (link.contains("rel=\"next\"")) {
assertEquals("<" + authzClient.getConfiguration().getAuthServerUrl() + uri + "?first=" + nextPage + "&max=" + max + ">; rel=\"next\"", link);
} else {
assertEquals("<" + authzClient.getConfiguration().getAuthServerUrl() + uri + "?first=" + (nextPage - max) + "&max=" + max + ">; rel=\"prev\"", link);
assertEquals("<" + authzClient.getConfiguration().getAuthServerUrl() + uri + "?first=" + (nextPage - max * 2) + "&max=" + max + ">; rel=\"prev\"", link);
}
}
} catch (IOException e) {

View file

@ -0,0 +1,159 @@
package org.keycloak.testsuite.ui.account2;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AccountRoles;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage;
import org.keycloak.testsuite.ui.account2.page.MyResourcesPage;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
public class MyResourcesTest extends BaseAccountPageTest {
private static final String[] userNames = new String[]{"alice", "jdoe", "bob"};
@Page
private MyResourcesPage myResourcesPage;
private AuthzClient authzClient;
private RealmRepresentation testRealm;
private CloseableHttpClient httpClient;
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
super.addTestRealms(testRealms);
testRealm = testRealms.get(0);
testRealm.setUserManagedAccessAllowed(true);
testRealm.setUsers(Arrays.asList(createUser("alice"),
createUser("jdoe"), createUser("bob")));
ClientRepresentation client = ClientBuilder.create()
.clientId("my-resource-server")
.authorizationServicesEnabled(true)
.serviceAccountsEnabled(true)
.secret("secret")
.name("My Resource Server")
.baseUrl("http://resourceserver.com")
.directAccessGrants().build();
testRealm.setClients(singletonList(client));
}
private UserRepresentation createUser(String userName) {
return UserBuilder.create()
.username(userName)
.enabled(true)
.password("password")
.role("account", AccountRoles.MANAGE_ACCOUNT)
.build();
}
@Before
public void before() {
httpClient = HttpClientBuilder.create().build();
}
@After
public void after() {
try {
httpClient.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Before
public void setup() throws Exception {
ClientResource resourceServer = getResourceServer();
authzClient = createAuthzClient(resourceServer.toRepresentation());
AuthorizationResource authorization = resourceServer.authorization();
for (int i = 0; i < 30; i++) {
ResourceRepresentation resource = new ResourceRepresentation();
resource.setOwnerManagedAccess(true);
final byte[] content = new JWSInput(authzClient.obtainAccessToken("jdoe", "password").getToken()).getContent();
final AccessToken accessToken = JsonSerialization.readValue(content, AccessToken.class);
resource.setOwner(accessToken.getSubject());
resource.setName("Resource " + i);
resource.setDisplayName("Display Name " + i);
resource.setIconUri("Icon Uri " + i);
resource.addScope("Scope A", "Scope B", "Scope C", "Scope D");
resource.setUri("http://resourceServer.com/resources/" + i);
try (Response response1 = authorization.resources().create(resource)) {
resource.setId(response1.readEntity(ResourceRepresentation.class).getId());
}
for (String scope : Arrays.asList("Scope A", "Scope B")) {
PermissionTicketRepresentation ticket = new PermissionTicketRepresentation();
ticket.setGranted(true);
ticket.setOwner(resource.getOwner().getId());
ticket.setRequesterName(userNames[i % userNames.length]);
ticket.setResource(resource.getId());
ticket.setScopeName(scope);
authzClient.protection("jdoe", "password").permission().create(ticket);
}
}
}
@Test
public void shouldShowMyResourcesInTable() {
myResourcesPage.assertCurrent();
assertEquals(10, myResourcesPage.getResourcesListCount());
}
private AuthzClient createAuthzClient(ClientRepresentation client) {
Map<String, Object> credentials = new HashMap<>();
credentials.put("secret", "secret");
return AuthzClient
.create(new Configuration(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth",
testRealm.getRealm(), client.getClientId(),
credentials, httpClient));
}
private ClientResource getResourceServer() {
ClientsResource clients = adminClient.realm(TEST).clients();
return clients.get(clients.findByClientId("my-resource-server").get(0).getId());
}
@Override
protected AbstractLoggedInPage getAccountPage() {
return myResourcesPage;
}
}

View file

@ -0,0 +1,21 @@
package org.keycloak.testsuite.ui.account2.page;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import java.util.List;
public class MyResourcesPage extends AbstractLoggedInPage {
@FindBy(id = "resourcesList")
private List<WebElement> resourcesList;
@Override
public String getPageId() {
return "resources";
}
public int getResourcesListCount() {
return resourcesList.size();
}
}

View file

@ -0,0 +1,4 @@
package org.keycloak.testsuite.ui.account2.page;
public class PhotozPage {
}

View file

@ -229,7 +229,7 @@
</div>
</div>
</div>
<div class="pf-l-gallery__item" style="display:none" id="landingMyResourcesCard">
<div class="pf-l-gallery__item" id="landingMyResourcesCard">
<div class="pf-c-card">
<div class="pf-c-card__header pf-c-content">
<h2><i class="pf-icon pf-icon-repository"></i>&nbsp${msg("myResources")}</h2>

View file

@ -72,7 +72,6 @@ export class AccountServiceClient {
return new Promise((resolve: AxiosResolve, reject: ErrorReject) => {
this.makeConfig(endpoint, config)
.then((config: AxiosRequestConfig) => {
console.log({config});
this.axiosRequest(config, resolve, reject);
}).catch( (error: AxiosError) => {
this.handleError(error);

View file

@ -0,0 +1,49 @@
import * as React from 'react';
import { Permission, PaginatedResources, Client } from './MyResourcesPage';
import { Msg } from '../../widgets/Msg';
export interface ResourcesTableProps {
resources: PaginatedResources;
}
export interface ResourcesTableState {
permissions: Map<number, Permission[]>;
}
export abstract class AbstractResourcesTable<S extends ResourcesTableState> extends React.Component<ResourcesTableProps, S> {
protected hasPermissions(row: number): boolean {
return (this.state.permissions.has(row)) && (this.state.permissions.get(row)!.length > 0);
}
private firstUser(row: number): string {
if (!this.hasPermissions(row)) return 'ERROR!!!!'; // should never happen
return this.state.permissions.get(row)![0].username;
}
protected numOthers(row: number): number {
if (!this.hasPermissions(row)) return -1; // should never happen
return this.state.permissions.get(row)!.length - 1;
}
public sharedWithUsersMessage(row: number): React.ReactNode {
if (!this.hasPermissions(row)) return (<React.Fragment><Msg msgKey='resourceNotShared' /></React.Fragment>);
// TODO: Not using a parameterized message because I want to use <strong> tag. Need to figure out a good solution to this.
if (this.numOthers(row) > 0) {
return (<React.Fragment><Msg msgKey='resourceSharedWith' /> <strong>{this.firstUser(row)}</strong> <Msg msgKey='and' /> <strong>{this.numOthers(row)}</strong> <Msg msgKey='otherUsers' />.</React.Fragment>)
} else {
return (<React.Fragment><Msg msgKey='resourceSharedWith' /> <strong>{this.firstUser(row)}</strong>.</React.Fragment>)
}
}
protected getClientName(client: Client): string {
if (client.hasOwnProperty('name') && client.name !== null && client.name !== '') {
return Msg.localize(client.name!);
} else {
return client.clientId;
}
}
}

View file

@ -0,0 +1,147 @@
/*
* Copyright 2019 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 {
Button,
Modal,
DataList,
DataListItemRow,
DataListItemCells,
DataListCell,
DataListItem,
ChipGroup,
ChipGroupToolbarItem,
Chip
} from '@patternfly/react-core';
import { EditAltIcon } from '@patternfly/react-icons';
import { Resource, Permission, Scope } from './MyResourcesPage';
import { Msg } from '../../widgets/Msg';
import { AccountServiceClient } from '../../account-service/account.service';
import { ContentAlert } from '../ContentAlert';
interface EditTheResourceProps {
resource: Resource;
permissions: Permission[];
onClose: (resource: Resource, row: number) => void;
row: number;
}
interface EditTheResourceState {
isOpen: boolean;
}
export class EditTheResource extends React.Component<EditTheResourceProps, EditTheResourceState> {
protected static defaultProps = { permissions: [], row: 0 };
public constructor(props: EditTheResourceProps) {
super(props);
this.state = {
isOpen: false,
};
}
private clearState(): void {
this.setState({
});
}
private handleToggleDialog = () => {
if (this.state.isOpen) {
this.setState({ isOpen: false });
} else {
this.clearState();
this.setState({ isOpen: true });
}
};
async deletePermission(permission: Permission, scope: Scope): Promise<void> {
permission.scopes.splice(permission.scopes.indexOf(scope), 1);
await AccountServiceClient.Instance.doPut(`/resources/${this.props.resource._id}/permissions`, {data: [permission]});
ContentAlert.success(Msg.localize('shareSuccess'));
this.props.onClose(this.props.resource, this.props.row);
}
public render(): React.ReactNode {
return (
<React.Fragment>
<Button variant="link" onClick={this.handleToggleDialog}>
<EditAltIcon /> Edit
</Button>
<Modal
title={'Edit the resource - ' + this.props.resource.name}
isLarge={true}
width={'45%'}
isOpen={this.state.isOpen}
onClose={this.handleToggleDialog}
actions={[
<Button key="done" variant="link" onClick={this.handleToggleDialog}>
<Msg msgKey='done' />
</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>
<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>
]}
/>
</DataListItemRow>
</DataListItem>
);
})}
</DataList>
</Modal>
</React.Fragment>
);
}
}

View file

@ -26,6 +26,7 @@ import {AccountServiceClient} from '../../account-service/account.service';
import {ResourcesTable} from './ResourcesTable';
import {ContentPage} from '../ContentPage';
import {Msg} from '../../widgets/Msg';
import { SharedResourcesTable } from './SharedResourcesTable';
export interface MyResourcesPageProps {
}
@ -112,7 +113,6 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
}
private hasPrevious(): boolean {
console.log('prev url=' + this.state.myResources.prevUrl);
if (this.isSharedWithMeTab()) {
return (this.state.sharedWithMe.prevUrl !== null) && (this.state.sharedWithMe.prevUrl !== '');
} else {
@ -137,9 +137,7 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
}
private fetchResources(url: string, extraParams?: Record<string, string|number>): void {
AccountServiceClient.Instance.doGet(url,
{params: extraParams}
)
AccountServiceClient.Instance.doGet(url, {params: extraParams})
.then((response: AxiosResponse<Resource[]>) => {
const resources: Resource[] = response.data;
resources.forEach((resource: Resource) => resource.shareRequests = []);
@ -148,15 +146,11 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
resources.forEach((resource: Resource) => resource.scopes = resource.scopes.map(this.makeScopeObj));
if (this.isSharedWithMeTab()) {
console.log('Shared With Me Resources: ');
this.setState({sharedWithMe: this.parseResourceResponse(response)});
this.setState({sharedWithMe: this.parseResourceResponse(response)}, this.fetchPending);
} else {
console.log('MyResources: ');
this.setState({myResources: this.parseResourceResponse(response)}, this.fetchPermissionRequests);
}
console.log({response});
})
});
}
private makeScopeObj = (scope: Scope): Scope => {
@ -164,7 +158,6 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
}
private fetchPermissionRequests = () => {
console.log('fetch permission requests');
this.state.myResources.data.forEach((resource: Resource) => {
this.fetchShareRequests(resource);
});
@ -173,16 +166,25 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
private fetchShareRequests(resource: Resource): void {
AccountServiceClient.Instance.doGet('/resources/' + resource._id + '/permissions/requests')
.then((response: AxiosResponse<Permission[]>) => {
//console.log('Share requests for ' + resource.name);
//console.log({response});
resource.shareRequests = response.data;
if (resource.shareRequests.length > 0) {
//console.log('forcing update');
this.forceUpdate();
}
});
}
private fetchPending = async () => {
const response = await AccountServiceClient.Instance.doGet(`/resources/pending-requests`);
response.data.forEach((pendingRequest: Resource) => {
this.state.sharedWithMe.data.forEach(resource => {
if (resource._id === pendingRequest._id) {
resource.shareRequests = [{username: 'me', scopes: pendingRequest.scopes}]
this.forceUpdate();
}
});
});
}
private parseResourceResponse(response: AxiosResponse<Resource[]>): PaginatedResources {
const links: string = response.headers.link;
const parsed: (parse.Links | null) = parse(links);
@ -200,7 +202,7 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
return {nextUrl: next, prevUrl: prev, data: resources};
}
private makeTab(eventKey: number, title: string, resources: PaginatedResources, noResourcesMessage: string): React.ReactNode {
private makeTab(eventKey: number, title: string, resources: PaginatedResources, sharedResourcesTab: boolean): React.ReactNode {
return (
<Tab eventKey={eventKey} title={Msg.localize(title)}>
<Stack gutter="md">
@ -213,7 +215,8 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
</Level>
</StackItem>
<StackItem isFilled>
<ResourcesTable resources={resources} noResourcesMessage={noResourcesMessage}/>
{!sharedResourcesTab && <ResourcesTable resources={resources}/>}
{sharedResourcesTab && <SharedResourcesTable resources={resources}/>}
</StackItem>
</Stack>
</Tab>
@ -224,8 +227,8 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
return (
<ContentPage title="resources" onRefresh={this.fetchInitialResources.bind(this)}>
<Tabs isFilled activeKey={this.state.activeTabKey} onSelect={this.handleTabClick}>
{this.makeTab(0, 'myResources', this.state.myResources, 'notHaveAnyResource')}
{this.makeTab(1, 'sharedwithMe', this.state.sharedWithMe, 'noResourcesSharedWithYou')}
{this.makeTab(0, 'myResources', this.state.myResources, false)}
{this.makeTab(1, 'sharedwithMe', this.state.sharedWithMe, true)}
</Tabs>
<Level gutter='md'>

View file

@ -0,0 +1,145 @@
/*
* Copyright 2019 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 { Button, Modal, Text, Badge, DataListItem, DataList, TextVariants, DataListItemRow, DataListItemCells, DataListCell, Chip } from '@patternfly/react-core';
import { UserCheckIcon } from '@patternfly/react-icons';
import { AccountServiceClient } from '../../account-service/account.service';
import { Msg } from '../../widgets/Msg';
import { ContentAlert } from '../ContentAlert';
import { Resource, Scope, Permission } from './MyResourcesPage';
interface PermissionRequestProps {
resource: Resource;
onClose: () => void;
}
interface PermissionRequestState {
isOpen: boolean;
}
export class PermissionRequest extends React.Component<PermissionRequestProps, PermissionRequestState> {
protected static defaultProps = { permissions: [], row: 0 };
public constructor(props: PermissionRequestProps) {
super(props);
this.state = {
isOpen: false,
};
}
private handleApprove = async (username: string, scopes: Scope[]) => {
this.handle(username, scopes, true);
};
private handleDeny = async (username: string, scopes: Scope[]) => {
this.handle(username, scopes);
}
private handle = async (username: string, scopes: Scope[], approve: boolean = false) => {
const id = this.props.resource._id
this.handleToggleDialog();
const permissionsRequest = await AccountServiceClient.Instance.doGet(`/resources/${id}/permissions`);
const userScopes = permissionsRequest.data.find((p: Permission) => p.username === username).scopes;
if (approve) {
userScopes.push(...scopes);
}
try {
await AccountServiceClient.Instance.doPut(`/resources/${id}/permissions`, { data: [{ username: username, scopes: userScopes }] })
ContentAlert.success(Msg.localize('shareSuccess'));
this.props.onClose();
} catch (e) {
console.error('Could not update permissions', e.error);
}
}
private handleToggleDialog = () => {
if (this.state.isOpen) {
this.setState({ isOpen: false });
} else {
this.setState({ isOpen: true });
}
};
public render(): React.ReactNode {
return (
<React.Fragment>
<Button variant="link" onClick={this.handleToggleDialog}>
<UserCheckIcon size="lg" />
<Badge>{this.props.resource.shareRequests.length}</Badge>
</Button>
<Modal
title={'Permission requests - ' + this.props.resource.name}
isLarge={true}
width="45%"
isOpen={this.state.isOpen}
onClose={this.handleToggleDialog}
actions={[
<Button key="close" variant="link" onClick={this.handleToggleDialog}>
<Msg msgKey="close" />
</Button>,
]}
>
<DataList aria-label={Msg.localize('permissionRequests')}>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key='permissions-name-header' width={5}>
<strong>Requestor</strong>
</DataListCell>,
<DataListCell key='permissions-requested-header' width={5}>
<strong><Msg msgKey='permissions' /> requested</strong>
</DataListCell>,
<DataListCell key='permission-request-header' width={5}>
</DataListCell>
]}
/>
</DataListItemRow>
{this.props.resource.shareRequests.map((shareRequest, i) =>
<DataListItem key={i} aria-labelledby="requestor">
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key={`requestor${i}`}>
<span>{shareRequest.firstName} {shareRequest.lastName}</span><br />
<Text component={TextVariants.small}>{shareRequest.email}</Text>
</DataListCell>,
<DataListCell key={`permissions${i}`}>
{shareRequest.scopes.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>
</DataListCell>
]}
/>
</DataListItemRow>
</DataListItem>
)}
</DataList>
</Modal>
</React.Fragment>
);
}
}

View file

@ -18,7 +18,6 @@ import * as React from 'react';
import {AxiosResponse} from 'axios';
import {
Badge,
DataList,
DataListItem,
DataListItemRow,
@ -29,27 +28,26 @@ import {
Level,
LevelItem,
Stack,
StackItem
StackItem,
Button
} from '@patternfly/react-core';
import { EditAltIcon, Remove2Icon, UserCheckIcon } from '@patternfly/react-icons';
import { Remove2Icon } from '@patternfly/react-icons';
import {AccountServiceClient} from '../../account-service/account.service';
import {PermissionRequest} from "./PermissionRequest";
import {ShareTheResource} from "./ShareTheResource";
import {Client, PaginatedResources, Permission, Resource} from "./MyResourcesPage";
import {Permission, Resource} from "./MyResourcesPage";
import { Msg } from '../../widgets/Msg';
import { ContentAlert } from '../ContentAlert';
import { ResourcesTableState, ResourcesTableProps, AbstractResourcesTable } from './AbstractResourceTable';
import { EditTheResource } from './EditTheResource';
export interface ResourcesTableState {
export interface CollapsibleResourcesTableState extends ResourcesTableState {
isRowOpen: boolean[];
permissions: Map<number, Permission[]>;
}
export interface ResourcesTableProps {
resources: PaginatedResources;
noResourcesMessage: string;
}
export class ResourcesTable extends React.Component<ResourcesTableProps, ResourcesTableState> {
export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesTableState> {
public constructor(props: ResourcesTableProps) {
super(props);
@ -67,11 +65,8 @@ export class ResourcesTable extends React.Component<ResourcesTableProps, Resourc
};
private fetchPermissions(resource: Resource, row: number): void {
console.log('**** fetchPermissions');
AccountServiceClient.Instance.doGet('resources/' + resource._id + '/permissions')
.then((response: AxiosResponse<Permission[]>) => {
console.log('Fetching Permissions row: ' + row);
console.log({response});
const newPermissions: Map<number, Permission[]> = new Map(this.state.permissions);
newPermissions.set(row, response.data);
this.setState({permissions: newPermissions});
@ -79,22 +74,6 @@ export class ResourcesTable extends React.Component<ResourcesTableProps, Resourc
);
}
private hasPermissions(row: number): boolean {
return (this.state.permissions.has(row)) && (this.state.permissions.get(row)!.length > 0);
}
private firstUser(row: number): string {
if (!this.hasPermissions(row)) return 'ERROR!!!!'; // should never happen
return this.state.permissions.get(row)![0].username;
}
private numOthers(row: number): number {
if (!this.hasPermissions(row)) return -1; // should never happen
return this.state.permissions.get(row)!.length - 1;
}
public sharedWithUsersMessage(row: number): React.ReactNode {
if (!this.hasPermissions(row)) return (<React.Fragment><Msg msgKey='resourceNotShared'/></React.Fragment>);
@ -112,7 +91,7 @@ export class ResourcesTable extends React.Component<ResourcesTableProps, Resourc
public render(): React.ReactNode {
return (
<DataList aria-label={Msg.localize('resources')}>
<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
@ -138,7 +117,7 @@ export class ResourcesTable extends React.Component<ResourcesTableProps, Resourc
/>
</DataListItemRow>
</DataListItem>
{(this.props.resources.data.length === 0) && <Msg msgKey={this.props.noResourcesMessage}/>}
{(this.props.resources.data.length === 0) && <Msg msgKey="notHaveAnyResource"/>}
{this.props.resources.data.map((resource: Resource, row: number) => {
return (
<DataListItem key={'resource-' + row} aria-labelledby={resource.name} isExpanded={this.state.isRowOpen[row]}>
@ -158,7 +137,12 @@ export class ResourcesTable extends React.Component<ResourcesTableProps, Resourc
<a href={resource.client.baseUrl}>{this.getClientName(resource.client)}</a>
</DataListCell>,
<DataListCell key={'permissionRequests-' + row} width={5}>
{resource.shareRequests.length > 0 && <a href={resource.client.baseUrl}><UserCheckIcon size='lg'/><Badge>{resource.shareRequests.length}</Badge></a>}
{resource.shareRequests.length > 0 &&
<PermissionRequest
resource={resource}
onClose={() => this.fetchPermissions(resource, row)}
></PermissionRequest>
}
</DataListCell>
]}
/>
@ -187,8 +171,18 @@ export class ResourcesTable extends React.Component<ResourcesTableProps, Resourc
onClose={this.fetchPermissions.bind(this)}
row={row}/>
</LevelItem>
<LevelItem><EditAltIcon/> Edit</LevelItem>
<LevelItem><Remove2Icon/> Remove</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>
@ -196,16 +190,7 @@ export class ResourcesTable extends React.Component<ResourcesTableProps, Resourc
</DataListContent>
</DataListItem>
)})}
</DataList>
);
}
private getClientName(client: Client): string {
if (client.hasOwnProperty('name') && client.name !== null && client.name !== '') {
return Msg.localize(client.name!);
} else {
return client.clientId;
}
}
}

View file

@ -0,0 +1,114 @@
/*
* 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 {
DataList,
DataListItem,
DataListItemRow,
DataListCell,
DataListItemCells,
ChipGroup,
ChipGroupToolbarItem,
Chip
} from '@patternfly/react-core';
import {PaginatedResources, Resource} from "./MyResourcesPage";
import { Msg } from '../../widgets/Msg';
import { AbstractResourcesTable, ResourcesTableState } from './AbstractResourceTable';
export interface ResourcesTableProps {
resources: PaginatedResources;
noResourcesMessage: string;
}
export class SharedResourcesTable extends AbstractResourcesTable<ResourcesTableState> {
public constructor(props: ResourcesTableProps) {
super(props);
this.state = {
permissions: new Map()
}
}
public render(): React.ReactNode {
return (
<DataList aria-label={Msg.localize('resources')}>
<DataListItem key='resource-header' aria-labelledby='resource-header'>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key='resource-name-header' width={2}>
<strong><Msg msgKey='resourceName'/></strong>
</DataListCell>,
<DataListCell key='application-name-header' width={2}>
<strong><Msg msgKey='application'/></strong>
</DataListCell>,
<DataListCell key='permission-header' width={2}/>,
<DataListCell key='requests-header' width={2}/>,
]}
/>
</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>
<DataListItemCells
dataListCells={[
<DataListCell key={'resourceName-' + row} width={2}>
<Msg msgKey={resource.name}/>
</DataListCell>,
<DataListCell key={'resourceClient-' + row} width={2}>
<a href={resource.client.baseUrl}>{this.getClientName(resource.client)}</a>
</DataListCell>,
<DataListCell key={'permissions-' + row} width={2}>
<ChipGroup>
<ChipGroupToolbarItem key='permissions' categoryName={Msg.localize('permissions')}>
{
resource.scopes.length > 0 && resource.scopes.map(scope => (
<Chip key={scope.name} isReadOnly>
{scope.displayName || scope.name}
</Chip>
))
}
</ChipGroupToolbarItem>
</ChipGroup>
</DataListCell>,
<DataListCell key={'pending-' + row} width={2}>
{resource.shareRequests.length > 0 &&
<ChipGroupToolbarItem key='permissions' categoryName={Msg.localize('pending')}>
{
resource.shareRequests[0].scopes.map(scope => (
<Chip key={scope.name} isReadOnly>
{scope.displayName || scope.name}
</Chip>
))
}
</ChipGroupToolbarItem>
}
</DataListCell>
]}
/>
</DataListItemRow>
</DataListItem>
))}
</DataList>
);
}
}

View file

@ -62,6 +62,6 @@ var content = [
label: 'resources',
modulePath: '/app/content/my-resources-page/MyResourcesPage',
componentName: 'MyResourcesPage',
hidden: true //!features.isMyResourcesEnabled
hidden: !features.isMyResourcesEnabled
}
];