Skip to content

Commit

Permalink
feat: add loading states with swr
Browse files Browse the repository at this point in the history
  • Loading branch information
Yatanvesh committed Oct 28, 2023
1 parent 7b0f80d commit 03ee375
Show file tree
Hide file tree
Showing 13 changed files with 474 additions and 85 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"react-icons": "^4.11.0",
"react-intl": "^6.5.1",
"react-tailwindcss-datepicker": "^1.6.6",
"swr": "^2.2.4",
"zustand": "^4.4.4"
},
"devDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import '@/styles/globals.scss';
import { Inter } from 'next/font/google';
import { AbstractIntlMessages, NextIntlClientProvider } from 'next-intl';

import { ToastProvider } from '@/components/toast-provider';

const inter = Inter({ subsets: ['latin'] });

const supportedLocales = ['en'];
Expand Down Expand Up @@ -31,7 +33,7 @@ export default async function RootLayout({
<html lang={locale}>
<body className={inter.className}>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
<ToastProvider>{children}</ToastProvider>
</NextIntlClientProvider>
</body>
</html>
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/dashboard/data-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface DataCardProps {
totalValue: string | number;
currentValue?: string;
percentage?: string;
loading?: boolean;
}

export function DataCard({
Expand All @@ -15,6 +16,7 @@ export function DataCard({
totalValue,
currentValue,
percentage,
loading,
}: DataCardProps) {
return (
<div className="flex items-center gap-4">
Expand All @@ -24,7 +26,13 @@ export function DataCard({
data-testid={`data-card-total-value-${metric}`}
className="text-3xl font-semibold text-neutral-content"
>
{totalValue}
{!loading && totalValue}
{loading && (
<span
data-testid="data-card-loader"
className="loading loading-spinner text-primary"
/>
)}
</h1>
{currentValue && percentage && (
<p className="text-neutral-content text-xs font-light flex gap-1.5 items-center">
Expand Down
160 changes: 95 additions & 65 deletions frontend/src/components/projects/[projectId]/applications-list.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,127 @@
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { useEffect } from 'react';
import { PencilFill, Plus } from 'react-bootstrap-icons';
import useSWR from 'swr';

import { handleRetrieveApplications, handleRetrievePromptConfigs } from '@/api';
import { Navigation } from '@/constants';
import { ApiError } from '@/errors';
import {
useApplications,
usePromptConfig,
useSetProjectApplications,
useSetPromptConfig,
} from '@/stores/project-store';
import { useShowError } from '@/stores/toast-store';
import { populateApplicationId, populateProjectId } from '@/utils/navigation';

export function ApplicationsList({ projectId }: { projectId: string }) {
const t = useTranslations('projectOverview');
const setProjectApplications = useSetProjectApplications();
const applications = useApplications(projectId);
const setPromptConfig = useSetPromptConfig();
const setProjectApplications = useSetProjectApplications();

const promptConfigs = usePromptConfig();
const setPromptConfig = useSetPromptConfig();

async function fetchApplications() {
const applicationsRes = await handleRetrieveApplications(projectId);
setProjectApplications(projectId, applicationsRes);
const showError = useShowError();

const promptConfigs = await Promise.all(
applicationsRes.map((application) =>
handleRetrievePromptConfigs({
projectId,
applicationId: application.id,
}),
const { isLoading } = useSWR(projectId, handleRetrieveApplications, {
onSuccess(data) {
setProjectApplications(projectId, data);
},
onError({ message }: ApiError) {
showError(message);
},
});

useSWR(
() => applications,
(applications) =>
Promise.all(
applications.map((application) =>
handleRetrievePromptConfigs({
projectId,
applicationId: application.id,
}),
),
),
{
onSuccess(data) {
data.forEach((promptConfig, index) => {
setPromptConfig(applications![index].id, promptConfig);
});
},
},
);

function renderTable() {
if (isLoading && !applications?.length) {
return (
<div className="w-full flex mb-8">
<span className="loading loading-bars mx-auto" />
</div>
);
}
if (!applications?.length) {
return null;
}

return (
<table className="custom-table mb-16">
<thead>
<tr>
<th>{t('name')}</th>
<th>{t('configs')}</th>
<th>{t('edit')}</th>
</tr>
</thead>
<tbody>
{applications.map(({ name, id }) => {
const applicationUrl = populateApplicationId(
populateProjectId(
Navigation.Applications,
projectId,
),
id,
);
return (
<tr key={id}>
<td>
<Link
data-testid="application-name-anchor"
href={applicationUrl}
>
{name}
</Link>
</td>
<td data-testid="application-prompt-config-count">
{promptConfigs[id]?.length}
</td>
<td className="flex justify-center">
<Link
data-testid="application-edit-anchor"
className="block"
href={applicationUrl}
>
<PencilFill className="w-3.5 h-3.5 text-secondary" />
</Link>
</td>
</tr>
);
})}
</tbody>
</table>
);
promptConfigs.forEach((promptConfig, index) => {
setPromptConfig(applicationsRes[index].id, promptConfig);
});
}

useEffect(() => {
void fetchApplications();
}, []);

return (
<div data-testid="project-application-list-container" className="mt-9">
<h2 className="font-semibold text-white text-xl ">
{t('applications')}
</h2>
<div className="custom-card">
<table className="custom-table">
<thead>
<tr>
<th>{t('name')}</th>
<th>{t('configs')}</th>
<th>{t('edit')}</th>
</tr>
</thead>
<tbody>
{applications?.map(({ name, id }) => {
const applicationUrl = populateApplicationId(
populateProjectId(
Navigation.Applications,
projectId,
),
id,
);
return (
<tr key={id}>
<td>
<Link
data-testid="application-name-anchor"
href={applicationUrl}
>
{name}
</Link>
</td>
<td data-testid="application-prompt-config-count">
{promptConfigs[id]?.length}
</td>
<td className="flex justify-center">
<Link
data-testid="application-edit-anchor"
className="block"
href={applicationUrl}
>
<PencilFill className="w-3.5 h-3.5 text-secondary" />
</Link>
</td>
</tr>
);
})}
</tbody>
</table>
<button className="mt-16 flex gap-2 items-center text-secondary hover:brightness-90">
<div className="custom-card flex flex-col">
{renderTable()}
<button className="flex gap-2 items-center text-secondary hover:brightness-90">
<Plus className="text-secondary w-4 h-4 hover:brightness-90" />
<span>{t('newApplication')}</span>
</button>
Expand Down
36 changes: 20 additions & 16 deletions frontend/src/components/projects/[projectId]/project-analytics.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { Activity, Cash } from 'react-bootstrap-icons';
import { DateValueType } from 'react-tailwindcss-datepicker';
import useSWR from 'swr';

import { handleProjectAnalytics } from '@/api';
import { DataCard } from '@/components/dashboard/data-card';
import { DatePicker } from '@/components/dashboard/date-picker';
import { ApiError } from '@/errors';
import { useShowError } from '@/stores/toast-store';
import { useDateFormat } from '@/stores/user-config-store';
import { ProjectAnalytics } from '@/types';

export function ProjectAnalytics({ projectId }: { projectId: string }) {
const t = useTranslations('projectOverview');
const dateFormat = useDateFormat();

const invokeErrorToast = useShowError();
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);

const [dateRange, setDateRange] = useState<DateValueType>({
startDate: oneWeekAgo,
endDate: new Date(),
});

const [analytics, setAnalytics] = useState<ProjectAnalytics | null>(null);

useEffect(() => {
(async () => {
const applicationAnalytics = await handleProjectAnalytics({
projectId,
fromDate: dateRange?.startDate,
toDate: dateRange?.endDate,
});
setAnalytics(applicationAnalytics);
})();
}, [dateRange]);
const { data: analytics, isLoading } = useSWR(
{
projectId,
fromDate: dateRange?.startDate,
toDate: dateRange?.endDate,
},
handleProjectAnalytics,
{
onError({ message }: ApiError) {
invokeErrorToast(message);
},
},
);

return (
<div data-testid="project-analytics-container">
Expand All @@ -55,6 +57,7 @@ export function ProjectAnalytics({ projectId }: { projectId: string }) {
totalValue={analytics?.totalAPICalls ?? ''}
percentage={'100'}
currentValue={'324'}
loading={isLoading}
/>
<div className="w-px h-12 bg-gray-200 mx-4" />
<DataCard
Expand All @@ -63,6 +66,7 @@ export function ProjectAnalytics({ projectId }: { projectId: string }) {
totalValue={`${analytics?.modelsCost ?? ''}$`}
percentage={'103'}
currentValue={'3.3'}
loading={isLoading}
/>
</div>
</div>
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/components/toast-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import { useToasts } from '@/stores/toast-store';

export function ToastProvider({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<Toast />
</>
);
}

function Toast() {
const toasts = useToasts();

return (
<div data-testid="toast-container" className="toast toast-center">
{toasts.map(({ type, message }, index) => (
<div
key={index}
data-testid="toast-message"
className={`alert ${type}`}
>
<span>{message}</span>
</div>
))}
</div>
);
}
Loading

0 comments on commit 03ee375

Please sign in to comment.