Skip to content

Commit

Permalink
fix(ui): Org overview (#376)
Browse files Browse the repository at this point in the history
Update the organisation overview page with summary count and table list
of projects
  • Loading branch information
bodinsamuel authored Nov 27, 2023
1 parent fcf80bc commit e2bf265
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 58 deletions.
2 changes: 2 additions & 0 deletions pkgs/api/src/routes/v0/catalog/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import get from './get.js';
import list from './list.js';
import summary from './summary/get.js';
import getUserActivities from './userActivities/get.js';

import type { FastifyPluginAsync } from 'fastify';

const fn: FastifyPluginAsync = async (f) => {
await f.register(list, { prefix: '/catalog' });
await f.register(summary, { prefix: '/catalog/summary' });
await f.register(get, { prefix: '/catalog/:tech_id' });
await f.register(getUserActivities, {
prefix: '/catalog/:tech_id/user_activities',
Expand Down
40 changes: 40 additions & 0 deletions pkgs/api/src/routes/v0/catalog/summary/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { schemaOrgId } from '@specfy/core';
import { catalogSummary } from '@specfy/models';
import { z } from 'zod';

import type { GetCatalogSummary } from '@specfy/models';

import { validationError } from '../../../../common/errors.js';
import { valPermissions } from '../../../../common/zod.js';

import type { FastifyPluginCallback, FastifyRequest } from 'fastify';

function QueryVal(req: FastifyRequest) {
return z
.object({ org_id: schemaOrgId })
.strict()
.superRefine(valPermissions(req, 'viewer'));
}

const fn: FastifyPluginCallback = (fastify, _, done) => {
fastify.get<GetCatalogSummary>('/', async function (req, res) {
const val = QueryVal(req).safeParse(req.query);
if (!val.success) {
return validationError(res, val.error);
}

const query: GetCatalogSummary['Querystring'] = val.data;

const catalog = await catalogSummary({ orgId: query.org_id });
console.log(catalog);

return res.status(200).send({
data: {
count: catalog.aggregations?.count.value || 0,
},
});
});
done();
};

export default fn;
21 changes: 21 additions & 0 deletions pkgs/app/src/api/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,34 @@ import { useQuery } from '@tanstack/react-query';

import type {
GetCatalog,
GetCatalogSummary,
GetCatalogUserActivities,
ListCatalog,
} from '@specfy/models';

import { fetchApi } from './fetch';
import { APIError, isError } from './helpers';

export function useCatalogSummary(opts: GetCatalogSummary['Querystring']) {
return useQuery({
queryKey: ['getCatalogSummary', opts.org_id],
queryFn: async (): Promise<GetCatalogSummary['Success']> => {
const { json, res } = await fetchApi<GetCatalogSummary>(
'/catalog/summary',
{
qp: opts,
}
);

if (res.status !== 200 || isError(json)) {
throw new APIError({ res, json });
}

return json;
},
});
}

export function useListCatalog(opts: ListCatalog['Querystring']) {
return useQuery({
queryKey: ['listCatalog', opts.org_id, opts.type],
Expand Down
21 changes: 21 additions & 0 deletions pkgs/app/src/components/Metric/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.metric {
display: flex;
gap: var(--spc);
align-items: baseline;

&.column {
flex-direction: column;
gap: var(--spc-xs);
}
}

.label {
color: var(--text-secondary);
}

.number {
font-size: var(--font-4xl);
font-weight: 500;
line-height: var(--lh-4xl);
color: var(--text-number);
}
27 changes: 27 additions & 0 deletions pkgs/app/src/components/Metric/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import classNames from 'classnames';

import cls from './index.module.scss';

export const Metric: React.FC<{
number: number;
label: React.ReactNode;
unit?: string;
labelPos?: 'right' | 'down';
className?: string;
}> = ({ number, label, unit, className, labelPos = 'right' }) => {
return (
<div
className={classNames(
cls.metric,
labelPos === 'down' && cls.column,
className
)}
>
<div className={cls.number}>
{number}
{unit}
</div>
<div className={cls.label}>{label}</div>
</div>
);
};
12 changes: 0 additions & 12 deletions pkgs/app/src/components/Project/List/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,6 @@
padding: var(--spc-6xl) var(--spc-6xl) var(--spc-4xl);
}

.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spc-l);
}

.actions {
display: flex;
gap: var(--spc-m);
}

.title {
font-size: var(--font);
Expand Down Expand Up @@ -49,5 +38,4 @@
.list {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
margin-top: var(--spc-5xl);
}
12 changes: 3 additions & 9 deletions pkgs/app/src/components/Project/List/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,15 @@ export const ProjectList: React.FC<{ orgId: string }> = ({ orgId }) => {

return (
<div className={cls.main}>
<div className={cls.header}>
<h2>Projects</h2>
<div>
{!brandNew && (
<div className={cls.actions}>
<Link to={`/${orgId}/_/project/new`}>
<Button display="primary">
<IconPlus /> New
</Button>
</Link>
<Flex grow>
<Input
before={<IconSearch />}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
</div>
</Flex>
)}
</div>

Expand Down
24 changes: 4 additions & 20 deletions pkgs/app/src/views/Org/Catalog/Show/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,15 @@
}

.block {
display: flex;
flex-direction: column;
gap: var(--spc-2xl);

&.chart {
margin: var(--spc-2xl) 0 var(--spc-2xl);
}
}


.metric {
display: flex;
gap: var(--spc);
align-items: baseline;
margin-bottom: var(--spc-2xl)
}

.label {
margin-bottom: var(--spc-l);
color: var(--text-secondary);
}

.number {
font-size: var(--font-4xl);
font-weight: 500;
line-height: var(--lh-4xl);
color: var(--text-number);
}

.heading {
display: flex;
gap: var(--spc-m);
Expand Down
26 changes: 10 additions & 16 deletions pkgs/app/src/views/Org/Catalog/Show/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ComponentIcon } from '@/components/Component/Icon';
import { ComponentTag } from '@/components/Component/Tag';
import { ContainerChild } from '@/components/Container';
import { Flex } from '@/components/Flex';
import { Metric } from '@/components/Metric';
import { NotFound } from '@/components/NotFound';
import { Tag } from '@/components/Tag';
import { Subdued } from '@/components/Text';
Expand Down Expand Up @@ -118,10 +119,7 @@ const UserActivities: React.FC<{
User Activities <Tag variant="light">90d</Tag>
</h3>

<div className={cls.metric}>
<div className={cls.number}>{data.users.length}</div>
<div className={cls.label}>users pushed code</div>
</div>
<Metric number={data.users.length} label="users pushed code" />
</div>
<Flex style={{ width: '100%', height: '100%' }} justify="flex-end">
<AreaChart height={80} width={250} data={histogram}>
Expand Down Expand Up @@ -284,10 +282,7 @@ const Projects: React.FC<{
<h3 className={cls.heading}>Projects</h3>
<div className={cls.grid}>
<div className={cls.block}>
<div className={cls.metric}>
<div className={cls.number}>{using.length}</div>
<div className={cls.label}>using</div>
</div>
<Metric number={using.length} label="using" />
{!projects ? (
<Skeleton />
) : (
Expand All @@ -310,10 +305,7 @@ const Projects: React.FC<{
</div>

<div className={cls.block}>
<div className={cls.metric}>
<div className={cls.number}>{notusing.length}</div>
<div className={cls.label}>not using</div>
</div>
<Metric number={notusing.length} label="not using" />

{!projects ? (
<Skeleton />
Expand Down Expand Up @@ -442,10 +434,12 @@ export const OrgCatalogShow: React.FC<{ org: ApiOrg }> = ({ org }) => {
))}
</Pie>
</PieChart>
<div>
<div className={cls.number}>{chart.pct}%</div>
<div className={cls.label}>Usage across {org.name}</div>
</div>
<Metric
number={chart.pct}
unit="%"
label={<>Usage across {org.name}</>}
labelPos="down"
/>
</Flex>
</div>
)}
Expand Down
10 changes: 10 additions & 0 deletions pkgs/app/src/views/Org/Overview/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.content {
padding: var(--spc-6xl) var(--spc-6xl) 0;
}

.metric {
flex-grow: 1;
padding: var(--spc-m) var(--spc-xl);
border: 1px solid var(--border1);
border-radius: var(--radius1);
}
35 changes: 34 additions & 1 deletion pkgs/app/src/views/Org/Overview/index.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
import { IconPlus } from '@tabler/icons-react';
import { useEffect } from 'react';
import { Helmet } from 'react-helmet-async';
import Skeleton from 'react-loading-skeleton';
import { Link } from 'react-router-dom';
import { useLocalStorage } from 'react-use';

import type { ApiOrg } from '@specfy/models';

import { useListProjects, useGetFlow } from '@/api';
import { useListProjects, useGetFlow, useCatalogSummary } from '@/api';
import { i18n } from '@/common/i18n';
import { useFlowStore } from '@/common/store';
import { titleSuffix } from '@/common/string';
import { Card } from '@/components/Card';
import { ContainerChild } from '@/components/Container';
import { Flex } from '@/components/Flex';
import { FlowOrg } from '@/components/Flow/FlowOrg';
import { Toolbar } from '@/components/Flow/Toolbar';
import { FlowWrapper } from '@/components/Flow/Wrapper';
import { Button } from '@/components/Form/Button';
import { ListActivity } from '@/components/ListActivity';
import { Metric } from '@/components/Metric';
import { OrgOnboarding } from '@/components/Org/Onboarding';
import { ProjectList } from '@/components/Project/List';
import type { RouteOrg } from '@/types/routes';

import cls from './index.module.scss';

export const OrgOverview: React.FC<{ org: ApiOrg; params: RouteOrg }> = ({
org,
params,
}) => {
const store = useFlowStore();
const res = useListProjects({ org_id: params.org_id });
const resFlow = useGetFlow({ org_id: params.org_id, flow_id: org.flowId });
const resSummary = useCatalogSummary({ org_id: params.org_id });
const [done] = useLocalStorage(`org.onboarding[${org.id}]`, false);

useEffect(() => {
Expand Down Expand Up @@ -53,6 +61,31 @@ export const OrgOverview: React.FC<{ org: ApiOrg; params: RouteOrg }> = ({
<Helmet title={`${org.name} ${titleSuffix}`} />
<ContainerChild leftLarge>
{!done && <OrgOnboarding org={org} key={org.id} />}
<Flex className={cls.content} gap="xl" column align="flex-start">
<Flex justify="space-between" grow>
<h2>{org.name} overview</h2>
<Link to={`/${params.org_id}/_/project/new`}>
<Button display="primary">
<IconPlus /> new project
</Button>
</Link>
</Flex>
<Flex gap="xl" grow>
<Metric
number={res.data?.pagination.totalItems || 0}
label="projects"
labelPos="down"
className={cls.metric}
/>
<Link to={`/${params.org_id}/_/catalog`} className={cls.metric}>
<Metric
number={resSummary.data?.data.count || 0}
label="technologies"
labelPos="down"
/>
</Link>
</Flex>
</Flex>
<ProjectList orgId={params.org_id}></ProjectList>
</ContainerChild>
<ContainerChild rightSmall>
Expand Down
Loading

0 comments on commit e2bf265

Please sign in to comment.