-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(admin-ui): add preferences pages to get list and update preferen…
…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
Showing
9 changed files
with
389 additions
and
7 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); |
Oops, something went wrong.