Skip to content

Commit

Permalink
Merge pull request #16 from aaron5670/feature/support-dynamic-configs
Browse files Browse the repository at this point in the history
Feature: Add support for Statsig Dynamic Configs
  • Loading branch information
aaron5670 authored Apr 24, 2024
2 parents aeb4e51 + 0993525 commit 202c50c
Show file tree
Hide file tree
Showing 20 changed files with 1,827 additions and 1,487 deletions.
19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "statsig-browser-extension",
"displayName": "Statsig Features and Experimentation",
"version": "1.6.1",
"version": "1.7.0",
"description": "A browser extension for the feature management and experimentation platform Statsig.",
"author": "(Aaron van den Berg <[email protected]>)",
"homepage": "https://aaronvandenberg.nl/",
Expand All @@ -13,15 +13,16 @@
"knip": "knip"
},
"dependencies": {
"@nextui-org/react": "^2.2.10",
"@nextui-org/react": "^2.3.5",
"@uidotdev/usehooks": "^2.4.1",
"@vahagn13/react-json-view": "^1.0.15",
"axios": "^1.6.8",
"framer-motion": "^11.0.25",
"framer-motion": "^11.1.7",
"immer": "^10.0.4",
"plasmo": "0.85.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^5.0.1",
"react-icons": "^5.1.0",
"react-modal-sheet": "^2.2.0",
"react-tooltip": "^5.26.3",
"swr": "^2.2.5",
Expand All @@ -30,20 +31,20 @@
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "4.2.1",
"@types/chrome": "0.0.266",
"@types/node": "20.12.5",
"@types/react": "18.2.74",
"@types/react-dom": "18.2.24",
"@types/node": "20.12.7",
"@types/react": "18.2.79",
"@types/react-dom": "18.2.25",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"autoprefixer": "^10.4.19",
"eslint": "^9.0.0",
"eslint-plugin-perfectionist": "^2.8.0",
"eslint-plugin-react": "^7.34.1",
"knip": "^5.9.1",
"knip": "^5.10.0",
"postcss": "^8.4.38",
"prettier": "3.2.5",
"tailwindcss": "^3.4.3",
"typescript": "5.4.4"
"typescript": "5.4.5"
},
"manifest": {
"host_permissions": [
Expand Down
2,495 changes: 1,138 additions & 1,357 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

245 changes: 245 additions & 0 deletions src/components/DynamicConfigs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import type {DynamicConfig} from "~types/statsig";
import type {ChangeEvent, Key} from "react";

import {
Button,
Chip,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger,
type Selection,
type SortDescriptor,
Spinner,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from "@nextui-org/react";
import {useLocalStorage} from "@uidotdev/usehooks";
import {ExternalLinkIcon} from "~components/icons/ExternalLinkIcon";
import BottomContent from "~components/tables/BottomContent";
import TopContent from "~components/tables/TopContent";
import {useStore} from "~store/useStore";
import React, {useCallback, useMemo, useState} from "react";

import {dynamicConfigColumns} from "./data";
import {VerticalDotsIcon} from "./icons/VerticalDotsIcon";
import {useDynamicConfigs} from "~hooks/useDynamicConfigs";

export default function DynamicConfigs() {
const {dynamicConfigs, isLoading} = useDynamicConfigs();
const [visibleColumns, setVisibleColumns] = useLocalStorage("dynamic-config-table-visible-columns", ["name", "tags", "actions"]);
const [rowsPerPage, setRowsPerPage] = useLocalStorage("dynamic-config-table-rows-per-page", 5);
const {setCurrentItemId, setItemSheetOpen} = useStore((state) => state);
const [filterValue, setFilterValue] = useState("");
const [statusFilter, setStatusFilter] = useState<Selection>("all");
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: "status",
direction: "ascending",
});
const [page, setPage] = useState(1);
const pages = Math.ceil(dynamicConfigs.length / rowsPerPage);
const hasSearchFilter = Boolean(filterValue);

const headerColumns = useMemo(() => {
return dynamicConfigColumns.filter((column) => Array.from(visibleColumns).includes(column.uid));
}, [visibleColumns]);

const filteredItems = useMemo(() => {
let filteredExperiments = [...dynamicConfigs];

if (hasSearchFilter) {
filteredExperiments = filteredExperiments.filter((experiment) =>
experiment.name.toLowerCase().includes(filterValue.toLowerCase()),
);
}
return filteredExperiments;
}, [dynamicConfigs.length, filterValue, statusFilter]);

const items = useMemo(() => {
const start = (page - 1) * rowsPerPage;
const end = start + rowsPerPage;

return filteredItems.slice(start, end);
}, [page, filteredItems, rowsPerPage]);

const setCurrentExperiment = (experimentId: string) => {
setCurrentItemId(experimentId);
setItemSheetOpen(true);
};

const sortedItems = () => {
return [...items].sort((a: DynamicConfig, b: DynamicConfig) => {
const first = a[sortDescriptor.column as keyof DynamicConfig] as number;
const second = b[sortDescriptor.column as keyof DynamicConfig] as number;
const cmp = first < second ? -1 : first > second ? 1 : 0;

return sortDescriptor.direction === "descending" ? -cmp : cmp;
});
};

const renderCell = useCallback((experiment: DynamicConfig, columnKey: Key) => {
const cellValue = experiment[columnKey as keyof DynamicConfig];

switch (columnKey) {
case "name":
return (
<p onClick={() => setCurrentExperiment(experiment.id)}>{experiment.name}</p>
);
case "status":
return (
<Chip
className="capitalize border-none gap-1 text-default-600"
color={experiment.isEnabled ? "success" : "danger"}
onClick={() => setCurrentExperiment(experiment.id)}
size="sm"
variant="dot"
>
{experiment.isEnabled ? "Enabled" : "Disabled"}
</Chip>
);
case "tags":
return (
<div className="flex flex-wrap gap-1">
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore*/}
{cellValue.map((tag: string) => (
<Chip
className="capitalize"
color="warning"
key={tag}
onClick={() => setCurrentExperiment(experiment.id)}
size="sm"
variant="dot"
>
{tag}
</Chip>
))}
</div>
);
case "isEnabled":
return (
<Chip
className="capitalize border-none gap-1 text-default-600"
color={experiment.isEnabled ? "success" : "danger"}
onClick={() => setCurrentExperiment(experiment.id)}
size="sm"
variant="dot"
>
{experiment.isEnabled ? "Enabled" : "Disabled"}
</Chip>
);
case "actions":
return (
<div className="relative flex justify-end items-center gap-2">
<Dropdown backdrop="opaque" className="bg-background border-1 border-default-200">
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon height={20} width={20}/>
</Button>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem onClick={() => setCurrentExperiment(experiment.id)}>View</DropdownItem>
<DropdownItem
as={'a'}
endContent={<ExternalLinkIcon/>}
href={`https://console.statsig.com/dynamic_configs/${experiment.id}`}
target="_blank"
>
Open on Statsig
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
);
default:
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<p onClick={() => setCurrentExperiment(experiment.id)}>{cellValue}</p>
);
}
}, []);

const onRowsPerPageChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
setRowsPerPage(Number(e.target.value));
setPage(1);
}, []);

const onSearchChange = useCallback((value?: string) => {
if (value) {
setFilterValue(value);
setPage(1);
} else {
setFilterValue("");
}
}, []);

return (
<Table
bottomContent={<BottomContent
hasSearchFilter={hasSearchFilter}
page={page}
setPage={setPage}
total={pages}
/>}
classNames={{
base: ["base-class"],
emptyWrapper: ["empty-wrapper-class"],
th: ["bg-transparent", "text-default-500"],
tr: ["hover:bg-default-50", "cursor-pointer"],
wrapper: ["max-h-[382px]", "max-w-3xl", "min-h-[242px]"],
}}
topContent={<TopContent
type="dynamicConfigs"
total={dynamicConfigs.length}
filterValue={filterValue}
hasSearchFilter={hasSearchFilter}
onRowsPerPageChange={onRowsPerPageChange}
onSearchChange={onSearchChange}
rowsPerPage={rowsPerPage}
setFilterValue={setFilterValue}
setStatusFilter={setStatusFilter}
setVisibleColumns={setVisibleColumns}
statusFilter={statusFilter}
visibleColumns={visibleColumns}
/>}
aria-label="Table with all Statsig Dynamic Configs"
bottomContentPlacement="outside"
fullWidth
isCompact
isHeaderSticky
onSortChange={setSortDescriptor}
removeWrapper
selectionMode="none"
sortDescriptor={sortDescriptor}
topContentPlacement="outside"
>
<TableHeader columns={headerColumns}>
{(column) => (
<TableColumn
align={column.uid === "actions" ? "center" : "start"}
allowsSorting={column.sortable}
key={column.uid}
>
{column.name}
</TableColumn>
)}
</TableHeader>
<TableBody
emptyContent={isLoading ? <Spinner className="h-full"/> : "No dynamic configs found"}
isLoading={!isLoading}
items={sortedItems()}
>
{(item: DynamicConfig) => (
<TableRow key={item.id}>
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
);
}
19 changes: 10 additions & 9 deletions src/components/Experiments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {useExperiments} from "~hooks/useExperiments";
import {useStore} from "~store/useStore";
import React, {useCallback, useMemo, useState} from "react";

import {columns, statusOptions} from "./data";
import {experimentColumns, experimentStatusOptions} from "./data";
import {VerticalDotsIcon} from "./icons/VerticalDotsIcon";

const statusMap: Record<string, string> = {
Expand All @@ -49,7 +49,7 @@ export default function Experiments() {
const {experiments, isLoading} = useExperiments();
const [visibleColumns, setVisibleColumns] = useLocalStorage("table-visible-columns", ["name", "status", "actions"]);
const [rowsPerPage, setRowsPerPage] = useLocalStorage("table-rows-per-page", 5);
const {setCurrentExperimentId, setExperimentSheetOpen} = useStore((state) => state);
const {setCurrentItemId, setItemSheetOpen} = useStore((state) => state);
const [filterValue, setFilterValue] = useState("");
const [statusFilter, setStatusFilter] = useState<Selection>("all");
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
Expand All @@ -61,7 +61,7 @@ export default function Experiments() {
const hasSearchFilter = Boolean(filterValue);

const headerColumns = useMemo(() => {
return columns.filter((column) => Array.from(visibleColumns).includes(column.uid));
return experimentColumns.filter((column) => Array.from(visibleColumns).includes(column.uid));
}, [visibleColumns]);

const filteredItems = useMemo(() => {
Expand All @@ -72,7 +72,7 @@ export default function Experiments() {
experiment.name.toLowerCase().includes(filterValue.toLowerCase()),
);
}
if (statusFilter !== "all" && Array.from(statusFilter).length !== statusOptions.length) {
if (statusFilter !== "all" && Array.from(statusFilter).length !== experimentStatusOptions.length) {
filteredExperiments = filteredExperiments.filter((experiment) =>
Array.from(statusFilter).includes(experiment.status),
);
Expand All @@ -88,8 +88,8 @@ export default function Experiments() {
}, [page, filteredItems, rowsPerPage]);

const setCurrentExperiment = (experimentId: string) => {
setCurrentExperimentId(experimentId);
setExperimentSheetOpen(true);
setCurrentItemId(experimentId);
setItemSheetOpen(true);
};

const sortedItems = () => {
Expand Down Expand Up @@ -212,7 +212,8 @@ export default function Experiments() {
wrapper: ["max-h-[382px]", "max-w-3xl", "min-h-[242px]"],
}}
topContent={<TopContent
experiments={experiments}
type="experiments"
total={experiments.length}
filterValue={filterValue}
hasSearchFilter={hasSearchFilter}
onRowsPerPageChange={onRowsPerPageChange}
Expand All @@ -224,7 +225,7 @@ export default function Experiments() {
statusFilter={statusFilter}
visibleColumns={visibleColumns}
/>}
aria-label="Table with all Statzig experiments"
aria-label="Table with all Statsig experiments"
bottomContentPlacement="outside"
fullWidth
isCompact
Expand All @@ -248,7 +249,7 @@ export default function Experiments() {
</TableHeader>
<TableBody
emptyContent={isLoading ? <Spinner className="h-full"/> : "No experiments found"}
isLoading={isLoading}
isLoading={!isLoading}
items={sortedItems()}
>
{(item: Experiment) => (
Expand Down
Loading

0 comments on commit 202c50c

Please sign in to comment.