Skip to content

Commit

Permalink
feat(admin-ui): add preferences pages to get list and update preferen…
Browse files Browse the repository at this point in the history
…ce (#497)

* feat: add preference list page

* feat: add preference details page to update preference

* fix: show value field is preference is undefined

* chore: fetch preferences and traits in outlet
  • Loading branch information
rsbh authored Feb 15, 2024
1 parent c5207b0 commit e709820
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 7 deletions.
8 changes: 5 additions & 3 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
"@raystack/frontier": "0.8.15",
"@stitches/react": "^1.2.8",
"@tanstack/react-table": "^8.9.3",
"dayjs": "^1.11.10",
"dotenv": "^16.0.3",
"ramda": "^0.29.0",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.9",
"react-loading-skeleton": "^3.4.0",
"react-router-dom": "^6.9.0",
"sonner": "^1.3.1",
"swr": "^2.1.2",
Expand Down
4 changes: 4 additions & 0 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const navigationItems: NavigationItemsTypes[] = [
name: "Roles",
to: `/roles`,
},
{
name: "Preferences",
to: `/preferences`,
},
];

function App() {
Expand Down
1 change: 0 additions & 1 deletion ui/src/containers/organisations.create/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export default function NewOrganisation() {
}, []);

const onSubmit = async (data: any) => {
console.log({ random: data });
try {
await client?.frontierServiceCreateOrganization(data);
toast.success("organisation added");
Expand Down
56 changes: 56 additions & 0 deletions ui/src/containers/preferences.list/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
V1Beta1Organization,
V1Beta1Preference,
V1Beta1PreferenceTrait,
} from "@raystack/frontier";
import type { ColumnDef } from "@tanstack/react-table";
import { createColumnHelper } from "@tanstack/react-table";
import Skeleton from "react-loading-skeleton";
import { Link } from "react-router-dom";

interface getColumnsOptions {
traits: V1Beta1PreferenceTrait[];
preferences: V1Beta1Preference[];
isLoading?: boolean;
}

export const getColumns: (
options: getColumnsOptions
) => ColumnDef<V1Beta1PreferenceTrait, any>[] = ({
traits,
preferences,
isLoading,
}) => {
return [
{
header: "Title",
accessorKey: "title",
filterVariant: "text",
cell: isLoading ? () => <Skeleton /> : (info) => info.getValue(),
footer: (props) => props.column.id,
},
{
header: "Action",
accessorKey: "name",
cell: isLoading
? () => <Skeleton />
: (info) => <Link to={`/preferences/${info.getValue()}`}>Edit</Link>,
footer: (props) => props.column.id,
},
{
header: "Value",
cell: isLoading
? () => <Skeleton />
: (info) => {
const name = info.row.original.name;
const currentPreference =
name && preferences.find((p) => p.name === name);
const value =
(currentPreference && currentPreference.value) ||
info.row.original.default;
return value;
},
footer: (props) => props.column.id,
},
];
};
191 changes: 191 additions & 0 deletions ui/src/containers/preferences.list/details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import PageHeader from "~/components/page-header";
import {
Button,
Flex,
Grid,
Separator,
Switch,
Text,
TextField,
} from "@raystack/apsara";
import { useCallback, useEffect, useState } from "react";
import { V1Beta1Preference, V1Beta1PreferenceTrait } from "@raystack/frontier";
import { useOutletContext, useParams } from "react-router-dom";
import { useFrontier } from "@raystack/frontier/react";
import Skeleton from "react-loading-skeleton";
import dayjs from "dayjs";
import * as R from "ramda";
import { toast } from "sonner";

interface ContextType {
preferences: V1Beta1Preference[];
traits: V1Beta1PreferenceTrait[];
isPreferencesLoading: boolean;
}

export function usePreferences() {
return useOutletContext<ContextType>();
}

interface PreferenceValueProps {
trait: V1Beta1PreferenceTrait;
value: string;
onChange: (v: string) => void;
}

function PreferenceValue({ value, trait, onChange }: PreferenceValueProps) {
if (R.has("checkbox")(trait)) {
const checked = value === "true";
return (
<Switch
// @ts-ignore
checked={checked}
onCheckedChange={(v: boolean) => onChange(v.toString())}
/>
);
} else if (R.or(R.has("text"), R.has("textarea"))(trait)) {
return (
<TextField value={value} onChange={(e) => onChange(e.target.value)} />
);
} else {
return null;
}
}

export default function PreferenceDetails() {
const { client } = useFrontier();
const { name } = useParams();
const [value, setValue] = useState("");
const [isActionLoading, setIsActionLoading] = useState(false);
const { preferences, traits, isPreferencesLoading } = usePreferences();
const preference = preferences?.find((p) => p.name === name);
const trait = traits?.find((t) => t.name === name);

const pageHeader = {
title: "Preference",
breadcrumb: [
{
href: `/preferences`,
name: `Preferences`,
},
{
href: `/preferences/${trait?.name}`,
name: `${trait?.title}`,
},
],
};

useEffect(() => {
const v =
preference?.value !== "" && preference?.value !== undefined
? preference?.value
: trait?.default;
setValue(v || "");
}, [preference?.value, trait?.default]);

const detailList = [
{
key: "Title",
value: trait?.title,
},
{
key: "Name",
value: trait?.name,
},
{
key: "Description",
value: trait?.description,
},
{
key: "Heading",
value: trait?.heading,
},
{
key: "Sub heading",
value: trait?.sub_heading,
},
{
key: "Resource type",
value: trait?.resource_type,
},
{
key: "Default value",
value: trait?.default,
},
{
key: "Last updated",
value:
preference?.updated_at &&
dayjs(preference?.updated_at).format("MMM DD, YYYY hh:mm:A"),
},
];

const onSave = useCallback(async () => {
setIsActionLoading(true);
try {
const resp = await client?.adminServiceCreatePreferences({
preferences: [
{
name,
value,
},
],
});
if (resp?.status === 200) {
toast.success("preference updated");
}
} catch (err) {
console.error(err);
toast.error("something went wrong");
} finally {
setIsActionLoading(false);
}
}, [client, name, value]);

return (
<Flex direction={"column"} style={{ width: "100%" }} gap="large">
<PageHeader
title={pageHeader.title}
breadcrumb={pageHeader.breadcrumb}
style={{ borderBottom: "1px solid var(--border-base)", gap: "16px" }}
/>
<Flex direction="column" gap="large" style={{ padding: "0 24px" }}>
{detailList.map((detailItem) =>
isPreferencesLoading ? (
<Grid columns={2} gap="small" key={detailItem.key}>
<Skeleton />
<Skeleton />
</Grid>
) : (
<Grid columns={2} gap="small" key={detailItem.key}>
<Text size={1} weight={500}>
{detailItem.key}
</Text>
<Text size={1}>{detailItem.value}</Text>
</Grid>
)
)}
<Separator />
{isPreferencesLoading ? (
<Skeleton />
) : (
<Text size={1} weight={500}>
Value
</Text>
)}
{trait ? (
<Flex direction={"column"} gap={"medium"}>
<PreferenceValue trait={trait} value={value} onChange={setValue} />
<Button
variant={"primary"}
onClick={onSave}
disabled={isActionLoading}
>
{isActionLoading ? "Saving..." : "Save"}
</Button>
</Flex>
) : null}
</Flex>
</Flex>
);
}
69 changes: 69 additions & 0 deletions ui/src/containers/preferences.list/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { DataTable, EmptyState, Flex } from "@raystack/apsara";
import { V1Beta1Preference, V1Beta1PreferenceTrait } from "@raystack/frontier";

import PageHeader from "~/components/page-header";
import { getColumns } from "./columns";
import { useOutletContext } from "react-router-dom";

const pageHeader = {
title: "Preferences",
breadcrumb: [],
};

interface ContextType {
preferences: V1Beta1Preference[];
traits: V1Beta1PreferenceTrait[];
isPreferencesLoading: boolean;
}

export function usePreferences() {
return useOutletContext<ContextType>();
}

export default function PreferencesList() {
const { preferences, traits, isPreferencesLoading } = usePreferences();

const tableStyle = traits?.length
? { width: "100%" }
: { width: "100%", height: "100%" };

const data = isPreferencesLoading
? [...new Array(5)].map((_, i) => ({
name: i,
title: "",
}))
: traits;

const columns = getColumns({
traits,
preferences,
isLoading: isPreferencesLoading,
});

return (
<DataTable
// @ts-ignore
data={data}
columns={columns}
emptyState={noDataChildren}
parentStyle={{ height: "calc(100vh - 60px)" }}
style={tableStyle}
>
<DataTable.Toolbar>
<PageHeader
title={pageHeader.title}
breadcrumb={pageHeader.breadcrumb}
/>
<DataTable.FilterChips style={{ padding: "8px 24px" }} />
</DataTable.Toolbar>
</DataTable>
);
}

export const noDataChildren = (
<EmptyState>
<div className="svg-container"></div>
<h3>0 traits</h3>
<div className="pera">Try creating new traits.</div>
</EmptyState>
);
Loading

0 comments on commit e709820

Please sign in to comment.