Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #376: Denote readonly buckets & collections in the sidebar. #382

Merged
merged 6 commits into from
Feb 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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