Skip to content

Commit

Permalink
feat: Cache (#6)
Browse files Browse the repository at this point in the history
* feat: Add Cache, InMemoryCache

* fix: Imports, unique keys

* fix: Handle missing zones

When a zone is part of a rule but no longer available on the API key,
make sure to render a message that allows us to remove the zone.

* fix: Add logger meta

* feat: Cache accounts and zones

* fix: Cache in db

* feat: Cache library

* fix: Add GET /cache, ?skipCache support

* fix: Tweak semaphore settings, log available api tokens

* fix: Add module to server logger

* feat: Cache management in Settings

* fix: Centralize logging initialization

* fix: Retry with exponential backoff

* fix: Do not skip accounts cache in zones

* fix: Hover

* fix: Full width zones

* fix: Pick account in Command

* fix: Words

* feat: DataTable page size
  • Loading branch information
pettermachado authored Oct 3, 2024
1 parent 6a702d4 commit f7c622f
Show file tree
Hide file tree
Showing 19 changed files with 690 additions and 203 deletions.
34 changes: 28 additions & 6 deletions api/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Router } from "express";
import bodyParser from "body-parser";
import { inspect } from "util";
import pino from "pino";

import { Api } from "../lib/soundtrack-api/index.js";
import {
Expand All @@ -14,8 +13,11 @@ import {
ZoneEvent,
} from "../lib/db/index.js";
import { Model } from "sequelize";
import { InMemoryCache } from "lib/cache/index.js";
import { SequelizeCache } from "lib/db/cache.js";
import { getLogger } from "lib/logger/index.js";

const logger = pino();
const logger = getLogger("api/index");

const jsonParser = bodyParser.json();

Expand Down Expand Up @@ -345,7 +347,10 @@ router.get("/events/:eventId/actions", async (req, res) => {
res.json(r.get("actions"));
});

const soundtrackApi = new Api();
const cache = process.env["DB_CACHE"]
? new SequelizeCache()
: new InMemoryCache();
const soundtrackApi = new Api({ cache });

router.get("/zones/:zoneId", async (req, res) => {
try {
Expand All @@ -359,7 +364,8 @@ router.get("/zones/:zoneId", async (req, res) => {

router.get("/zones", async (req, res) => {
try {
const zones = await soundtrackApi.getZones();
const skipCache = req.query["skipCache"] === "true";
const zones = await soundtrackApi.getZones(skipCache);
res.json(zones);
} catch (e) {
logger.error("Failed to get zones: " + e);
Expand All @@ -369,7 +375,11 @@ router.get("/zones", async (req, res) => {

router.get("/accounts/:accountId/library", async (req, res) => {
try {
const library = await soundtrackApi.getLibrary(req.params.accountId);
const skipCache = req.query["skipCache"] === "true";
const library = await soundtrackApi.getLibrary(
req.params.accountId,
skipCache,
);
res.json(library);
} catch (e) {
logger.error("Failed to get library: " + e);
Expand Down Expand Up @@ -414,7 +424,8 @@ router.get("/accounts/:accountId", async (req, res) => {

router.get("/accounts", async (req, res) => {
try {
const accounts = await soundtrackApi.getAccounts();
const skipCache = req.query["skipCache"] === "true";
const accounts = await soundtrackApi.getAccounts(skipCache);
res.json(accounts);
} catch (e) {
logger.error("Failed to get accounts: " + e);
Expand All @@ -436,6 +447,17 @@ router.get("/assignable/:id", async (req, res) => {
}
});

router.delete("/cache", async (req, res) => {
logger.info("Clearing cache");
await cache.clear();
res.sendStatus(200);
});

router.get("/cache", async (req, res) => {
const count = await cache.count();
res.json({ count });
});

router.all("*", (req, res) => {
res.sendStatus(404);
});
Expand Down
49 changes: 45 additions & 4 deletions app/components/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
CaretDownIcon,
CaretUpIcon,
CheckIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "@radix-ui/react-icons";
Expand All @@ -41,15 +42,26 @@ import {
import { Label } from "~/components/ui/label";
import { PopoverClose } from "@radix-ui/react-popover";
import { Skeleton } from "./ui/skeleton";
import { Command, CommandInput, CommandItem } from "./ui/command";
import { CommandEmpty, CommandList } from "cmdk";
import {
Command,
CommandInput,
CommandItem,
CommandEmpty,
CommandList,
} from "./ui/command";
import { cn } from "~/lib/utils";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from "./ui/tooltip";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";

type BulkAction<TData> = {
key: Key | null | undefined;
Expand Down Expand Up @@ -224,6 +236,35 @@ export const DataTable = function DataTable<TData, TValue>({
</div>
{pagination && table.getPageCount() > 0 && (
<div className="flex items-center space-x-2">
<div className="flex items-center">
<p className="text-sm text-muted-foreground mr-1">Per page</p>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
{table.getState().pagination.pageSize}
<ChevronDownIcon className="ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{[10, 25, 100].map((pageSize) => {
return (
<DropdownMenuCheckboxItem
key={pageSize}
checked={
table.getState().pagination.pageSize === pageSize
}
onCheckedChange={(checked) => {
if (!checked) return;
table.setPageSize(pageSize);
}}
>
{pageSize}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<p className="text-sm text-muted-foreground">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
Expand Down Expand Up @@ -312,10 +353,10 @@ function ColumnFilter<TData>(props: { id: string; column: Column<TData> }) {
<CommandInput placeholder="Filter" />
<CommandEmpty>No filter</CommandEmpty>
<CommandList>
{sortedUniqueValues.map((v) => {
{sortedUniqueValues.map((v, i) => {
return (
<CommandItem
key={v}
key={v + i}
onSelect={() =>
column.setFilterValue(
currentFilterValue !== v ? v : null,
Expand Down
2 changes: 1 addition & 1 deletion app/components/ExternalLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function ExternalLink(
<a href={props.href} target="_blank" rel="noreferrer" className={cx}>
{props.text && <span>{props.text}</span>}
{props.children}
<ExternalLinkIcon className="ml-1 text-slate-400 hover:text-black flex-shrink-0" />
<ExternalLinkIcon className="ml-1 text-slate-400 hover:text-primary flex-shrink-0" />
</a>
);
}
Expand Down
14 changes: 10 additions & 4 deletions app/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import { cn } from "~/lib/utils";

type PageProps = {
breadcrumbs?: BreadcrumbItemData[];
noWrap?: boolean;
};

export default function Page(props: PageProps & PropsWithChildren) {
const { breadcrumbs, children } = props;
const { breadcrumbs, noWrap, children } = props;
return (
<div className="px-3">
<div className="bg-navbar text-navbar-foreground py-2.5 -mx-3 px-3">
Expand All @@ -29,14 +30,19 @@ export default function Page(props: PageProps & PropsWithChildren) {
</div>
</div>
</div>
<div className="max-w-screen-lg m-auto py-3">
<PageWrap className="mt-3">
{breadcrumbs && <Breadcrumbs items={breadcrumbs} className="mb-3" />}
{children}
</div>
</PageWrap>
{noWrap ? children : <PageWrap>{children}</PageWrap>}
</div>
);
}

export function PageWrap(props: { className?: string } & PropsWithChildren) {
const cx = cn("max-w-screen-lg m-auto", props.className);
return <div className={cx}>{props.children}</div>;
}

function TopNavLink(props: NavLinkProps) {
const { className, ...rest } = props;
const cx = cn(className, styles.topNavLink);
Expand Down
43 changes: 31 additions & 12 deletions app/components/columns/zone.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { AccountZone, Zone } from "~/types";
import { AccountZone, ZoneEvent } from "~/types";
import { Checkbox } from "~/components/ui/checkbox";
import { Button } from "~/components/ui/button";
import { AccountLink, ZoneLink } from "../ExternalLinks";
import { MinusCircledIcon, PlusCircledIcon } from "@radix-ui/react-icons";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";

export type ZoneRowAction = "add" | "remove";

export type ZoneRowActionData = { zoneId: string; accountId: string };

export type ZoneRow = {
zone: AccountZone;
zoneEvent: Omit<ZoneEvent, "id">;
zone: AccountZone | undefined;
action: "add" | "remove";
onAction: (action: ZoneRowAction, zone: Zone) => void;
onAction: (action: ZoneRowAction, data: ZoneRowActionData) => void;
};

export const columns: ColumnDef<ZoneRow>[] = [
Expand Down Expand Up @@ -40,9 +44,11 @@ export const columns: ColumnDef<ZoneRow>[] = [
{
id: "account",
header: "Account",
accessorFn: ({ zone }) => zone.account.businessName,
accessorFn: ({ zone }) => zone?.account.businessName,
cell: (props) => {
return <AccountLink data={props.row.original.zone.account} />;
return props.row.original.zone ? (
<AccountLink data={props.row.original.zone.account} />
) : null;
},
meta: {
filter: "select",
Expand All @@ -51,7 +57,7 @@ export const columns: ColumnDef<ZoneRow>[] = [
{
id: "location",
header: "Location",
accessorFn: ({ zone }) => zone.location.name,
accessorFn: ({ zone }) => zone?.location.name,
meta: {
filter: "select",
},
Expand All @@ -60,16 +66,31 @@ export const columns: ColumnDef<ZoneRow>[] = [
id: "zone",
header: "Zone",
filterFn: "includesString",
accessorFn: ({ zone }) => zone.name,
accessorFn: ({ zoneEvent, zone }) => zone?.name ?? zoneEvent.zoneId,
cell: (props) => {
return (
return props.row.original.zone ? (
<ZoneLink
data={{
name: props.row.original.zone.name,
zoneId: props.row.original.zone.id,
accountId: props.row.original.zone.account.id,
}}
/>
) : (
<p className="w-[150px] truncate">
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost">Zone not available</Button>
</PopoverTrigger>
<PopoverContent className="overflow-auto text-sm">
<p className="font-bold">Zone not available</p>
<p>
Could not load data for zone:{" "}
{props.row.original.zoneEvent.zoneId}
</p>
</PopoverContent>
</Popover>
</p>
);
},
},
Expand All @@ -79,14 +100,12 @@ export const columns: ColumnDef<ZoneRow>[] = [
enableColumnFilter: false,
enableSorting: false,
cell: ({ row }) => {
const { zone, onAction, action } = row.original;
const { zoneEvent, onAction, action } = row.original;
return (
<Button
size="sm"
variant="ghost"
onClick={() => {
onAction(row.original.action, zone);
}}
onClick={() => onAction(action, zoneEvent)}
>
{action === "add" ? <PlusCircledIcon /> : <MinusCircledIcon />}
</Button>
Expand Down
3 changes: 3 additions & 0 deletions app/fetchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Account,
AccountLibrary,
Assignable,
CacheMetadata,
Event,
EventAction,
Run,
Expand Down Expand Up @@ -80,6 +81,8 @@ export const accountLibraryFetcher: Fetcher<AccountLibrary, string> = (url) =>
export const assignableFetcher: Fetcher<Assignable, string> = (url) =>
defaultFetcher(url).then(toAssignable);

export const cacheFetcher: Fetcher<CacheMetadata, string> = defaultFetcher;

export const errorHandler = async (res: Response) => {
if (!res.ok) {
const data = await res.json();
Expand Down
Loading

0 comments on commit f7c622f

Please sign in to comment.