Skip to content

Commit

Permalink
Fix #376: Denote readonly buckets & collections in the sidebar. (#382)
Browse files Browse the repository at this point in the history
  • Loading branch information
n1k0 authored Feb 3, 2017
1 parent c825ed0 commit ca9e891
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 58 deletions.
8 changes: 8 additions & 0 deletions css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ h1 {
padding-top: 1em;
}

.sidebar-filters > .panel-body {
padding: 0 15px 0 15px;
}

.list-group-item .glyphicon.glyphicon-lock {
color: #888;
}

.breadcrumbs {
font-size: 1.2em;
text-transform: lowercase;
Expand Down
10 changes: 9 additions & 1 deletion interfaces/external-modules/kinto-http.d.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ declare module "kinto-http" {
body: Object,
};

declare type PermissionEntry = {
resource_name: "bucket" | "group" | "collection" | "record",
id: string,
bucket_id: string,
collection_id?: string,
permissions: string[],
};

declare class KintoClient {
remote: string;
defaultReqOptions: {
Expand All @@ -45,7 +53,7 @@ declare module "kinto-http" {
batch(): Promise<BatchResponse[]>;
fetchServerInfo(): Promise<Object>;
listBuckets(): Promise<ListResponseBody<Resource>>;
listPermissions(): Promise<ListResponseBody<Object>>;
listPermissions(): Promise<ListResponseBody<PermissionEntry>>;
}

declare class Bucket {
Expand Down
119 changes: 80 additions & 39 deletions src/components/Sidebar.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* @flow */
import type { SessionState, RouteParams, RouteLocation } from "../types";
import type { SessionState, RouteParams, RouteLocation, BucketEntry } from "../types";

import React, { Component } from "react";

Expand Down Expand Up @@ -33,7 +33,9 @@ function CollectionMenuEntry(props) {
<div className={classes}>
<SideBarLink name="collection:records" params={{bid, cid}} currentPath={currentPath}
className="">
<i className="glyphicon glyphicon-align-justify"/>
{collection.readonly
? <i className="glyphicon glyphicon-lock"/>
: <i className="glyphicon glyphicon-align-justify"/>}
{cid}
</SideBarLink>
<SideBarLink name="collection:attributes" params={{bid, cid}} currentPath={currentPath}
Expand Down Expand Up @@ -69,45 +71,84 @@ function BucketCollectionsMenu(props) {
);
}

function BucketsMenu(props) {
const {currentPath, buckets, bid, cid} = props;
return (
<div>
<div className="panel panel-default">
<div className="list-group">
<SideBarLink name="bucket:create" currentPath={currentPath}>
<i className="glyphicon glyphicon-plus"/>
Create bucket
</SideBarLink>
type BucketsMenuProps = {
currentPath: string,
buckets: BucketEntry[],
bid: string,
cid: string,
};

class BucketsMenu extends Component {
props: BucketsMenuProps;

state: {
hideReadOnly: boolean,
};

constructor(props: BucketsMenuProps) {
super(props);
this.state = {hideReadOnly: false};
}

toggleReadOnly = () => {
this.setState({hideReadOnly: !this.state.hideReadOnly});
};

render() {
const {currentPath, buckets, bid, cid} = this.props;
const {hideReadOnly} = this.state;
console.log(buckets);
return (
<div>
<div className="panel panel-default">
<div className="list-group">
<SideBarLink name="bucket:create" currentPath={currentPath}>
<i className="glyphicon glyphicon-plus"/>
Create bucket
</SideBarLink>
</div>
</div>
<div className="panel panel-default sidebar-filters">
<div className="panel-body checkbox">
<label>
<input type="checkbox" value={this.state.hideReadOnly}
onChange={this.toggleReadOnly}/>
{" "}Hide readonly buckets
</label>
</div>
</div>
{
buckets
.filter((bucket) => !(hideReadOnly && bucket.readonly))
.map((bucket, i) => {
const {id, collections} = bucket;
const current = bid === id;
return (
<div key={i} className={`panel panel-${current ? "info": "default"} bucket-menu`}>
<div className="panel-heading">
{bucket.readonly
? <i className="glyphicon glyphicon-lock"/>
: <i className={`glyphicon glyphicon-folder-${current ? "open" : "close"}`} />}
<strong>{id}</strong> bucket
<SideBarLink name="bucket:attributes" params={{bid: id}} currentPath={currentPath}
className="bucket-menu-entry-edit"
title="Manage bucket">
<i className="glyphicon glyphicon-cog"/>
</SideBarLink>
</div>
<BucketCollectionsMenu
bucket={bucket}
collections={collections}
currentPath={currentPath}
bid={bid}
cid={cid} />
</div>
);
})
}
</div>
{
buckets.map((bucket, i) => {
const {id, collections} = bucket;
const current = bid === id;
return (
<div key={i} className={`panel panel-${current ? "info": "default"} bucket-menu`}>
<div className="panel-heading">
<i className={`glyphicon glyphicon-folder-${current ? "open" : "close"}`} />
<strong>{id}</strong> bucket
<SideBarLink name="bucket:attributes" params={{bid: id}} currentPath={currentPath}
className="bucket-menu-entry-edit"
title="Manage bucket">
<i className="glyphicon glyphicon-cog"/>
</SideBarLink>
</div>
<BucketCollectionsMenu
bucket={bucket}
collections={collections}
currentPath={currentPath}
bid={bid}
cid={cid} />
</div>
);
})
}
</div>
);
);
}
}

export default class Sidebar extends Component {
Expand Down
71 changes: 57 additions & 14 deletions src/sagas/session.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* @flow */
import type { ActionType, GetStateFn, SagaGen } from "../types";
import type { PermissionEntry } from "kinto-http";
import type { ActionType, BucketEntry, CollectionEntry, GetStateFn, SagaGen } from "../types";

import { push as updatePath } from "react-router-redux";
import { call, put } from "redux-saga/effects";
Expand Down Expand Up @@ -34,25 +35,53 @@ export function* sessionLogout(getState: GetStateFn, action: ActionType<typeof a
yield call(clearSession);
}

function expandBucketsCollections(buckets, permissions) {
export function expandBucketsCollections(buckets: BucketEntry[], permissions: PermissionEntry[]): BucketEntry[] {
// Create a copy to avoid mutating the source object
const bucketsCopy = clone(buckets);

// Augment the list of bucket and collections with the ones retrieved from
// the /permissions endpoint
for (const permission of permissions) {
// Add any missing bucket to the current list
let bucket = bucketsCopy.find(x => x.id === permission.bucket_id);
let bucket = bucketsCopy.find(b => b.id === permission.bucket_id);
if (!bucket) {
bucket = {id: permission.bucket_id, collections: []};
bucket = {
id: permission.bucket_id,
collections: [],
permissions: [],
readonly: true,
};
bucketsCopy.push(bucket);
}
// Add any missing collection to the current bucket collections list; note
// that this will expose collections we have shared records within too.
// We're dealing with bucket permissions
if (permission.resource_name === "bucket") {
bucket.permissions = permission.permissions;
bucket.readonly = !bucket.permissions.some((bp) => {
return ["write", "collection:create"].includes(bp);
});
}
if ("collection_id" in permission) {
const collection = bucket.collections.find(x => x.id === permission.collection_id);
// Add any missing collection to the current bucket collections list; note
// that this will expose collections we have shared records within too.
let collection = bucket.collections.find(c => c.id === permission.collection_id);
if (!collection) {
bucket.collections.push({id: permission.collection_id});
collection = {
id: permission.collection_id,
permissions: [],
readonly: true,
};
bucket.collections.push(collection);
}
// We're dealing with collection permissions
if (permission.resource_name === "collection") {
collection.permissions = permission.permissions;
collection.readonly = !collection.permissions.some((cp) => {
return ["write", "record:create"].includes(cp);
});
}
// If this collection is writable, mark its parent bucket writable
if (!collection.readonly) {
bucket.readonly = false;
}
}
}
Expand Down Expand Up @@ -90,19 +119,33 @@ export function* listBuckets(getState: GetStateFn, action: ActionType<typeof act
batch.bucket(id).listCollections();
}
});
let buckets = data.map((bucket, index) => {
const {data: collections=[]} = responses[index].body;
return {id: bucket.id, collections};
let buckets: BucketEntry[] = data.map((bucket, index) => {
// Initialize received collections with default permissions and readonly
// information.
const {data: rawCollections} = responses[index].body;
const collections: CollectionEntry[] = rawCollections.map(collection => {
return {
...collection,
permissions: [],
readonly: true,
};
});
// Initialize the list of permissions and readonly flag for this bucket;
// when the permissions endpoint is enabled, we'll fill these with the
// retrieved data.
return {id: bucket.id, collections, permissions: [], readonly: true};
});

// If the Kinto API version allows it, retrieves all permissions
if ("permissions_endpoint" in serverInfo.capabilities) {
const {data: permissions} = yield call([client, client.listPermissions]);
buckets = expandBucketsCollections(buckets, permissions);
yield put(actions.permissionsListSuccess(permissions));
}
else {
yield put(notificationActions.notifyInfo("Permissions endpoint is not enabled on server."));
} else {
yield put(notificationActions.notifyInfo([
"Permissions endpoint is not enabled on server, ",
"listed resources in the sidebar might be incomplete."
].join("")));
}

yield put(actions.bucketsSuccess(buckets));
Expand Down
15 changes: 14 additions & 1 deletion src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,25 @@ export type TokenAuth = {

export type SagaGen = Generator<*,void,*>;

export type CollectionEntry = {
id: string,
permissions: string[],
readonly: boolean,
};

export type BucketEntry = {
id: string,
permissions: string[],
collections: CollectionEntry[],
readonly: boolean,
};

export type SessionState = {
busy: boolean,
auth: ?AuthData;
authenticated: boolean,
permissions: ?PermissionsListEntry[],
buckets: Object[],
buckets: BucketEntry[],
serverInfo: ServerInfo,
redirectURL: ?string,
};
Expand Down
Loading

0 comments on commit ca9e891

Please sign in to comment.