* WIP - got one API test to pass yay * adds group update endpoint * added group policies * adds groups.list API * adds groups.info * remove comment * WIP * tests for delete * adds group membership list * adds tests for groups list * add and remove user endpoints for group * ask some questions * fix up some issues around primary keys * remove export from group permissions Co-Authored-By: Tom Moor <tom.moor@gmail.com> * remove random file * only create events on actual updates, add tests to ensure * adds uniqueness validation to group name * throw validation errors on model and let it pass through the controller * fix linting * WIP * WIP * WIP * WIP * WIP basic edit and delete * basic CRUD for groups and memberships in place * got member counts working * add member count and limit the number of users sent over teh wire to 6 * factor avatar with AvatarWithPresence into its own class * wip * WIP avatars in group lists * WIP collection groups * add and remove group endpoints * wip add collection groups * wip get group adding to collections to work * wip get updating collection group memberships to work * wip get new group modal working * add tests for collection index * include collection groups in the withmemberships scope * tie permissions to group memberships * remove unused import * Update app/components/GroupListItem.js update title copy Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update server/migrations/20191211044318-create-groups.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update server/api/groups.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update server/api/groups.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/menus/CollectionMenu.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update server/models/Group.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * minor fixes * Update app/scenes/CollectionMembers/AddGroupsToCollection.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/menus/GroupMenu.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/menus/GroupMenu.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/menus/GroupMenu.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/scenes/Collection.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/scenes/CollectionMembers/CollectionMembers.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/scenes/GroupNew.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/scenes/GroupNew.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/scenes/Settings/Groups.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update server/api/documents.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * Update app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js Co-Authored-By: Tom Moor <tom.moor@gmail.com> * address comments * WIP - getting websocket stuff up and running * socket event for group deletion * wrapped up cascading deletes * lint * flow * fix: UI feedback * fix: Facepile size * fix: Lots of missing await's * Allow clicking facepile on group list item to open members * remove unused route push, grammar * fix: Remove bad analytics events feat: Add group events to audit log * collection. -> collections. * Add groups to entity websocket events (sync create/update/delete) between clients * fix: Users should not be able to see groups they are not a member of * fix: Not caching errors in UI when changing group memberships * fix: Hide unusable UI * test * fix: Tweak language * feat: Automatically open 'add member' modal after creating group Co-authored-by: Tom Moor <tom.moor@gmail.com>
202 lines
5.3 KiB
JavaScript
202 lines
5.3 KiB
JavaScript
// @flow
|
||
import * as React from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
import { capitalize } from 'lodash';
|
||
import styled from 'styled-components';
|
||
import Time from 'shared/components/Time';
|
||
import ListItem from 'components/List/Item';
|
||
import Avatar from 'components/Avatar';
|
||
import Event from 'models/Event';
|
||
|
||
type Props = {
|
||
event: Event,
|
||
};
|
||
|
||
const description = event => {
|
||
switch (event.name) {
|
||
case 'api_keys.create':
|
||
return (
|
||
<React.Fragment>
|
||
Created the API token <strong>{event.data.name}</strong>
|
||
</React.Fragment>
|
||
);
|
||
case 'api_keys.delete':
|
||
return (
|
||
<React.Fragment>
|
||
Revoked the API token <strong>{event.data.name}</strong>
|
||
</React.Fragment>
|
||
);
|
||
case 'teams.create':
|
||
return 'Created the team';
|
||
case 'shares.create':
|
||
case 'shares.revoke':
|
||
return (
|
||
<React.Fragment>
|
||
{capitalize(event.verbPastTense)} a{' '}
|
||
<Link to={`/share/${event.modelId || ''}`}>public link</Link> to the{' '}
|
||
<Link to={`/doc/${event.documentId}`}>{event.data.name}</Link>{' '}
|
||
document
|
||
</React.Fragment>
|
||
);
|
||
case 'users.create':
|
||
return (
|
||
<React.Fragment>{event.data.name} created an account</React.Fragment>
|
||
);
|
||
case 'users.invite':
|
||
return (
|
||
<React.Fragment>
|
||
{capitalize(event.verbPastTense)} {event.data.name} (<a
|
||
href={`mailto:${event.data.email || ''}`}
|
||
>
|
||
{event.data.email || ''}
|
||
</a>)
|
||
</React.Fragment>
|
||
);
|
||
case 'users.suspend':
|
||
return (
|
||
<React.Fragment>
|
||
Suspended <strong>{event.data.name}’s</strong> account
|
||
</React.Fragment>
|
||
);
|
||
case 'users.activate':
|
||
return (
|
||
<React.Fragment>
|
||
Unsuspended <strong>{event.data.name}’s</strong> account
|
||
</React.Fragment>
|
||
);
|
||
case 'users.promote':
|
||
return (
|
||
<React.Fragment>
|
||
Made <strong>{event.data.name}</strong> an admin
|
||
</React.Fragment>
|
||
);
|
||
case 'users.demote':
|
||
return (
|
||
<React.Fragment>
|
||
Made <strong>{event.data.name}</strong> a member
|
||
</React.Fragment>
|
||
);
|
||
case 'users.delete':
|
||
return 'Deleted their account';
|
||
case 'groups.create':
|
||
return (
|
||
<React.Fragment>
|
||
Created the group <strong>{event.data.name}</strong>
|
||
</React.Fragment>
|
||
);
|
||
case 'groups.update':
|
||
return (
|
||
<React.Fragment>
|
||
Update the group <strong>{event.data.name}</strong>
|
||
</React.Fragment>
|
||
);
|
||
case 'groups.delete':
|
||
return (
|
||
<React.Fragment>
|
||
Deleted the group <strong>{event.data.name}</strong>
|
||
</React.Fragment>
|
||
);
|
||
case 'collections.add_user':
|
||
case 'collections.add_group':
|
||
return (
|
||
<React.Fragment>
|
||
Granted <strong>{event.data.name}</strong> access to a{' '}
|
||
<Link to={`/collections/${event.collectionId || ''}`}>
|
||
collection
|
||
</Link>
|
||
</React.Fragment>
|
||
);
|
||
case 'collections.remove_user':
|
||
case 'collections.remove_group':
|
||
return (
|
||
<React.Fragment>
|
||
Revoked <strong>{event.data.name}</strong> access to a{' '}
|
||
<Link to={`/collections/${event.collectionId || ''}`}>
|
||
collection
|
||
</Link>
|
||
</React.Fragment>
|
||
);
|
||
default:
|
||
}
|
||
|
||
if (event.documentId) {
|
||
if (event.name === 'documents.delete') {
|
||
return (
|
||
<React.Fragment>
|
||
Deleted the <strong>{event.data.title}</strong> document
|
||
</React.Fragment>
|
||
);
|
||
}
|
||
return (
|
||
<React.Fragment>
|
||
{capitalize(event.verbPastTense)} the{' '}
|
||
<Link to={`/doc/${event.documentId}`}>{event.data.title}</Link> document
|
||
</React.Fragment>
|
||
);
|
||
}
|
||
if (event.collectionId) {
|
||
if (event.name === 'collections.delete') {
|
||
return (
|
||
<React.Fragment>
|
||
Deleted the <strong>{event.data.name}</strong> collection
|
||
</React.Fragment>
|
||
);
|
||
}
|
||
return (
|
||
<React.Fragment>
|
||
{capitalize(event.verbPastTense)} the{' '}
|
||
<Link to={`/collections/${event.collectionId || ''}`}>
|
||
{event.data.name}
|
||
</Link>{' '}
|
||
collection
|
||
</React.Fragment>
|
||
);
|
||
}
|
||
if (event.userId) {
|
||
return (
|
||
<React.Fragment>
|
||
{capitalize(event.verbPastTense)} the user {event.data.name}
|
||
</React.Fragment>
|
||
);
|
||
}
|
||
return '';
|
||
};
|
||
|
||
const EventListItem = ({ event }: Props) => {
|
||
return (
|
||
<ListItem
|
||
key={event.id}
|
||
title={event.actor.name}
|
||
image={<Avatar src={event.actor.avatarUrl} size={32} />}
|
||
subtitle={
|
||
<React.Fragment>
|
||
{description(event)} <Time dateTime={event.createdAt} /> ago ·{' '}
|
||
<strong>{event.name}</strong>
|
||
</React.Fragment>
|
||
}
|
||
actions={
|
||
event.actorIpAddress ? (
|
||
<IP>
|
||
<a
|
||
href={`http://geoiplookup.net/ip/${event.actorIpAddress}`}
|
||
target="_blank"
|
||
rel="noreferrer noopener"
|
||
>
|
||
{event.actorIpAddress}
|
||
</a>
|
||
</IP>
|
||
) : (
|
||
undefined
|
||
)
|
||
}
|
||
/>
|
||
);
|
||
};
|
||
|
||
const IP = styled('span')`
|
||
color: ${props => props.theme.textTertiary};
|
||
font-size: 12px;
|
||
`;
|
||
|
||
export default EventListItem;
|