Skip to content

Commit

Permalink
Merge pull request #4262 from cloud-gov/4258-user-org-reporting
Browse files Browse the repository at this point in the history
Make org/user data available via reports interface
  • Loading branch information
svenaas authored Oct 5, 2023
2 parents 676e150 + 7c93682 commit 160e605
Show file tree
Hide file tree
Showing 18 changed files with 457 additions and 30 deletions.
4 changes: 3 additions & 1 deletion admin-client/src/Router.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@
page('/organizations/new', queryString, render(Pages.Organization.New));
page('/organizations/:id', queryString, render(Pages.Organization.Show));
page('/organizations/:id/edit', queryString, render(Pages.Organization.Edit));
page('/organizations-report', queryString, render(Pages.Organization.Report));
page('/reports/organizations', queryString, render(Pages.Organization.OrgsReport));
page('/reports/published-sites', queryString, render(Pages.Organization.PublishedSitesReport));
page('/reports/users', queryString, render(Pages.User.UsersReport));
page('/reports', queryString, render(Pages.Reports));
page('/tasks', queryString, render(Pages.Tasks));
page('*', render(Pages.NotFound));
Expand Down
12 changes: 12 additions & 0 deletions admin-client/src/helpers/downloadCSV.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable import/prefer-default-export */
export const downloadCSV = async (fetchCSV, filename) => {
const csv = await fetchCSV();
const blob = new Blob([csv], { type: 'application/octet-stream' });
const aElement = document.createElement('a');
aElement.setAttribute('download', filename);
const href = URL.createObjectURL(blob);
aElement.href = href;
aElement.setAttribute('target', '_blank');
aElement.click();
URL.revokeObjectURL(href);
};
24 changes: 22 additions & 2 deletions admin-client/src/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,19 @@ async function fetchOrganizations(query = {}) {
}

async function fetchOrganizationsReport(query = {}) {
return get('/organizations-report', query).catch(() => []);
return get('/reports/organizations', query).catch(() => []);
}

async function fetchOrganizationsReportCSV() {
return getAttachedFile('/organizations-report.csv').catch(() => []);
return getAttachedFile('/reports/organizations.csv').catch(() => []);
}

async function fetchPublishedSitesReport(query = {}) {
return get('/reports/published-sites', query).catch(() => []);
}

async function fetchPublishedSitesReportCSV() {
return getAttachedFile('/reports/published-sites.csv').catch(() => []);
}

async function updateOrganization(id, params) {
Expand Down Expand Up @@ -270,6 +278,14 @@ async function fetchUsers(query = {}) {
return get('/users', query).catch(() => []);
}

async function fetchUsersReport(query = {}) {
return get('/reports/users', query).catch(() => []);
}

async function fetchUsersReportCSV() {
return getAttachedFile('/reports/users.csv').catch(() => []);
}

async function inviteUser(params) {
return post('/users/invite', params);
}
Expand Down Expand Up @@ -309,6 +325,8 @@ export {
fetchOrganizations,
fetchOrganizationsReport,
fetchOrganizationsReportCSV,
fetchPublishedSitesReport,
fetchPublishedSitesReportCSV,
updateOrganization,
deactivateOrganization,
activateOrganization,
Expand All @@ -323,6 +341,8 @@ export {
fetchUserEnvironmentVariables,
fetchUser,
fetchUsers,
fetchUsersReport,
fetchUsersReportCSV,
inviteUser,
resendInvite,
logout,
Expand Down
4 changes: 3 additions & 1 deletion admin-client/src/pages/Reports.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<GridContainer>
<PageTitle>Reports</PageTitle>
<ul>
<li><a href="/organizations-report">Organizations With Published Sites</a></li>
<li><a href="/reports/organizations">Organizations</a></li>
<li><a href="/reports/published-sites">Organizations With Published Sites</a></li>
<li><a href="/reports/users">Users</a></li>
</ul>
</GridContainer>
25 changes: 25 additions & 0 deletions admin-client/src/pages/organization/OrgsReport.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script>
import { downloadCSV } from '../../helpers/downloadCSV';
import { fetchOrganizationsReport, fetchOrganizationsReportCSV } from '../../lib/api';
import { PaginatedQueryPage, DataTable } from '../../components';
</script>

<PaginatedQueryPage path="reports/organizations" title="Organizations" query={fetchOrganizationsReport} noSearch let:data>
<div>
<button type="button" class="usa-button margin-left-1" on:click={() => downloadCSV(fetchOrganizationsReportCSV, 'organizations.csv')}>Download CSV of All Organizations</button>
</div>
<DataTable data={data}>
<tr slot="header">
<th scope="col">Organization</th>
<th scope="col">Agency</th>
</tr>
<tr slot="item" let:item={org}>
<td>
<a href="/organizations/{org.id}">{org.name}</a>
</td>
<td>
{org.agency}
</td>
</tr>
</DataTable>
</PaginatedQueryPage>
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
<script>
import { fetchOrganizationsReport, fetchOrganizationsReportCSV } from '../../lib/api';
import { downloadCSV } from '../../helpers/downloadCSV';
import { fetchPublishedSitesReport, fetchPublishedSitesReportCSV } from '../../lib/api';
import { PaginatedQueryPage, DataTable } from '../../components';
async function downloadCSV() {
const csv = await fetchOrganizationsReportCSV();
const blob = new Blob([csv], { type: 'application/octet-stream' });
const aElement = document.createElement('a');
aElement.setAttribute('download', 'organization-report.csv');
const href = URL.createObjectURL(blob);
aElement.href = href;
aElement.setAttribute('target', '_blank');
aElement.click();
URL.revokeObjectURL(href);
}
const fields = {
organization: {
type: 'select-auto',
Expand All @@ -25,9 +14,9 @@
};
</script>

<PaginatedQueryPage path="organizations-report" title="Organizations With Published Sites" query={fetchOrganizationsReport} {fields} noSearch let:data>
<PaginatedQueryPage path="reports/published-sites" title="Organizations With Published Sites" query={fetchPublishedSitesReport} {fields} noSearch let:data>
<div>
<button type="button" class="usa-button margin-left-1" on:click={downloadCSV}>Download CSV of All Published Sites</button>
<button type="button" class="usa-button margin-left-1" on:click={() => downloadCSV(fetchPublishedSitesReportCSV, 'published-sites.csv')}>Download CSV of All Published Sites</button>
</div>
<DataTable data={data}>
<tr slot="header">
Expand Down
3 changes: 2 additions & 1 deletion admin-client/src/pages/organization/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { default as Edit } from './Edit.svelte';
export { default as New } from './New.svelte';
export { default as Index } from './Index.svelte';
export { default as Show } from './Show.svelte';
export { default as Report } from './Report.svelte';
export { default as OrgsReport } from './OrgsReport.svelte';
export { default as PublishedSitesReport } from './PublishedSitesReport.svelte';
62 changes: 62 additions & 0 deletions admin-client/src/pages/user/UsersReport.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script>
import { formatDistanceStrict } from 'date-fns';
import { downloadCSV } from '../../helpers/downloadCSV';
import { fetchUsersReport, fetchUsersReportCSV } from '../../lib/api';
import { PaginatedQueryPage, DataTable } from '../../components';
</script>

<PaginatedQueryPage path="reports/organizations" title="Users" query={fetchUsersReport} noSearch let:data>
<div>
<button type="button" class="usa-button margin-left-1" on:click={() => downloadCSV(fetchUsersReportCSV, 'users.csv')}>Download CSV of All Users</button>
</div>
<DataTable data={data}>
<tr slot="header">
<th scope="col">ID</th>
<th scope="col">Github Email</th>
<th scope="col">UAA Email</th>
<th scope="col">Organizations</th>
<th scope="col">Details</th>
<th scope="col">Created</th>
<th scope="col">Signed In</th>
</tr>
<tr slot="item" let:item={user}>
<td>
<a href="/organizations/{user.id}">{user.id}</a>
</td>
<td>
{#if user.email }
{user.email}
{/if}
</td>
<td>
{#if user.UAAIdentity }
{user.UAAIdentity.email}
{/if}
</td>
<td>
{#if user.OrganizationRoles.length > 0}
{
user.OrganizationRoles.map((orgRole) => `${orgRole.Organization.name}`).join(', ')
}
{/if}
</td>
<td>
{#if user.OrganizationRoles.length > 0}
{
user.OrganizationRoles.map((orgRole) => `${orgRole.Organization.name}: ${orgRole.Role.name}`).join(', ')
}
{/if}
</td>
<td>
{ formatDistanceStrict(new Date(user.createdAt), new Date(), { addSuffix: true, roundingMethod: 'floor' }) }
</td>
<td>
{#if user.signedInAt }
{ formatDistanceStrict(new Date(user.signedInAt), new Date(), { addSuffix: true, roundingMethod: 'floor' }) }
{:else }
never
{/if }
</td>
</tr>
</DataTable>
</PaginatedQueryPage>
1 change: 1 addition & 0 deletions admin-client/src/pages/user/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as Show } from './Show.svelte';
export { default as Index } from './Index.svelte';
export { default as Invite } from './Invite.svelte';
export { default as UsersReport } from './UsersReport.svelte';
2 changes: 1 addition & 1 deletion api/admin/controllers/domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ module.exports = wrapHandlers({

const parser = new json2csv.Parser({ fields });
const csv = parser.parse(domains);
res.attachment('organizations-report.csv');
res.attachment('published-sites.csv');
return res.send(csv);
},

Expand Down
36 changes: 36 additions & 0 deletions api/admin/controllers/organization.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const json2csv = require('@json2csv/plainjs');
const { serialize, serializeMany } = require('../../serializers/organization');
const { paginate, wrapHandlers } = require('../../utils');
const { Organization, Event } = require('../../models');
Expand Down Expand Up @@ -41,6 +42,41 @@ module.exports = wrapHandlers({
return res.json(json);
},

async listOrdered(req, res) {
const { limit, page } = req.query;

const scopes = ['byName'];

const pagination = await paginate(Organization.scope(scopes), serializeMany, { limit, page });

const json = {
meta: {},
...pagination,
};

return res.json(json);
},

async listOrderedCSV(req, res) {
const orgs = await Organization.scope('byName').findAll();

const fields = [
{
label: 'Organization',
value: 'name',
},
{
label: 'Agency',
value: 'agency',
},
];

const parser = new json2csv.Parser({ fields });
const csv = parser.parse(orgs);
res.attachment('organizations.csv');
return res.send(csv);
},

async findById(req, res) {
const {
params: { id },
Expand Down
54 changes: 54 additions & 0 deletions api/admin/controllers/user.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const json2csv = require('@json2csv/plainjs');
const {
Organization, Site, User, Event,
} = require('../../models');
Expand Down Expand Up @@ -52,6 +53,59 @@ module.exports = wrapHandlers({
return res.json(json);
},

async listForUsersReport(req, res) {
const { limit, page } = req.query;

const serialize = users => userSerializer.serializeMany(users, true);

const scopes = ['withUAAIdentity', 'withOrganizationRoles'];

const pagination = await paginate(User.scope(scopes), serialize, { limit, page });

const json = {
meta: {},
...pagination,
};

return res.json(json);
},

async listForUsersReportCSV(req, res) {
const users = await User.scope(['withUAAIdentity', 'withOrganizationRoles']).findAll();

const fields = [
{
label: 'ID',
value: 'id',
},
{
label: 'Email',
value: 'UAAIdentity.email',
},
{
label: 'Organizations',
value: (user => user.OrganizationRoles.map(orgRole => `${orgRole.Organization.name}`).join('|')),
},
{
label: 'Details',
value: (user => user.OrganizationRoles.map(orgRole => `${orgRole.Organization.name}: ${orgRole.Role.name}`).join(', ')),
},
{
label: 'Created',
value: 'createdAt',
},
{
label: 'Last Signed In',
value: 'signedInAt',
},
];

const parser = new json2csv.Parser({ fields });
const csv = parser.parse(users);
res.attachment('users.csv');
return res.send(csv);
},

async findById(req, res) {
const {
params: { id },
Expand Down
8 changes: 6 additions & 2 deletions api/admin/routers/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ apiRouter.put('/organizations/:id', AdminControllers.Organization.update);
apiRouter.post('/organizations/:id/deactivate', authorize(['pages.admin']), AdminControllers.Organization.deactivate);
apiRouter.post('/organizations/:id/activate', AdminControllers.Organization.activate);
apiRouter.delete('/organization/:org_id/user/:user_id', authorize(['pages.admin']), AdminControllers.OrganizationRole.destroy);
apiRouter.get('/organizations-report', AdminControllers.Domain.listPublished);
apiRouter.get('/organizations-report.csv', AdminControllers.Domain.listPublishedCSV);
apiRouter.get('/reports/organizations', AdminControllers.Organization.listOrdered);
apiRouter.get('/reports/organizations.csv', AdminControllers.Organization.listOrderedCSV);
apiRouter.get('/reports/published-sites', AdminControllers.Domain.listPublished);
apiRouter.get('/reports/published-sites.csv', AdminControllers.Domain.listPublishedCSV);
apiRouter.get('/reports/users', AdminControllers.User.listForUsersReport);
apiRouter.get('/reports/users.csv', AdminControllers.User.listForUsersReportCSV);
apiRouter.put('/organization-role', AdminControllers.OrganizationRole.update);
apiRouter.get('/roles', AdminControllers.Role.list);
apiRouter.get('/sites', AdminControllers.Site.list);
Expand Down
6 changes: 6 additions & 0 deletions api/models/organization.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ const associate = ({
}],
}],
}));

Organization.addScope('byName', {
order: [
['name', 'ASC'],
],
});
};

module.exports = (sequelize, DataTypes) => {
Expand Down
Loading

0 comments on commit 160e605

Please sign in to comment.