KEYCLOAK-13121 added the basic functionality
This commit is contained in:
parent
da1138a8d2
commit
7580be8708
14 changed files with 724 additions and 74 deletions
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package org.keycloak.testsuite.ui.account2.page;
|
||||
|
||||
public class PhotozPage {
|
||||
}
|
|
@ -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> ${msg("myResources")}</h2>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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'>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -62,6 +62,6 @@ var content = [
|
|||
label: 'resources',
|
||||
modulePath: '/app/content/my-resources-page/MyResourcesPage',
|
||||
componentName: 'MyResourcesPage',
|
||||
hidden: true //!features.isMyResourcesEnabled
|
||||
hidden: !features.isMyResourcesEnabled
|
||||
}
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue