beta key list page (#565)

* init beta key list

* show key only if user is creator or referrer

* fix console errors with keys

* partially working filters

* sort header fixes

* remove unused stuff

* updates
This commit is contained in:
BFlat authored and GitHub committed 2024-05-17 07:53:51 +07:00
1 parent 9592e84a11
commit 8b06cbefa8
11 files changed
+460 -25

No files matched your search

@@ -0,0 +1,58 @@
import MakeRequest from "../MakeRequest";
import { Pagination } from "../_common/SharedFetchTypes";
export interface BetaKey {
created_at: Date;
creator: {
default_avatar: {
color_index: number;
image_index: number;
};
display_name: string;
gravatar_hash: string;
user_token: string;
username: string;
};
key_value: string;
maybe_redeemed_at?: Date;
maybe_redeemer?: {
default_avatar: {
color_index: number;
image_index: number;
};
display_name: string;
gravatar_hash: string;
user_token: string;
username: string;
} | null;
maybe_referrer?: {
default_avatar: {
color_index: number;
image_index: number;
};
display_name: string;
gravatar_hash: string;
user_token: string;
username: string;
} | null;
product: string;
token: string;
}
export interface ListBetaKeysRequest {}
export interface ListBetaKeysResponse {
beta_keys: BetaKey[];
pagination: Pagination;
success: boolean;
}
export const ListBetaKeys = MakeRequest<
string,
ListBetaKeysRequest,
ListBetaKeysResponse,
{}
>({
method: "GET",
routingFunction: () => "/v1/beta_keys/list",
});
@@ -1,11 +1,11 @@
import MakeRequest from "../MakeRequest";
import { MediaFile } from "./GetMedia";
import { LazyPagination } from "../_common/SharedFetchTypes";
import { Pagination } from "../_common/SharedFetchTypes";
export interface ListMediaFilesRequest {}
export interface ListMediaFilesResponse {
pagination: LazyPagination;
pagination: Pagination;
success: boolean;
results: MediaFile[];
}
+1
View File
@@ -18,6 +18,7 @@
"@react-spring/types": "^9.7.3",
"@react-spring/web": "^9.7.3",
"@storyteller/components": "0.1.0",
"@tanstack/react-table": "^8.17.3",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
@@ -23,6 +23,7 @@ export interface SelectProps extends ReactSelectProps {
options: Option[];
onChange?: (value: any) => void;
required?: boolean;
className?: string;
}
export default function Select({
@@ -40,13 +41,13 @@ export default function Select({
const isMulti = Array.isArray(value);
const findVal = (opts: Option[], nest = 0): Option | undefined => {
let val: Option | undefined;
opts.forEach((option) => {
opts.forEach(option => {
if (!val) {
if (option.options) {
val = findVal(option.options,++nest);
val = findVal(option.options, ++nest);
} else if (option.value === value) {
val = option
}
val = option;
}
}
});
return val;
@@ -55,11 +56,17 @@ export default function Select({
const valueLabel = findVal(options)?.label || "";
const onChange = (option: any, x: any) => {
if (Array.isArray(option)) {
inChange({ target: {value: option.map(({ value = "" }) => value), name, type: "select" } });
inChange({
target: {
value: option.map(({ value = "" }) => value),
name,
type: "select",
},
});
} else {
inChange({ target: {value: option.value, name, type: "select" } });
inChange({ target: { value: option.value, name, type: "select" } });
}
}
};
const className = `${icon ? " input-icon" : ""}${
small ? " select-small" : ""
}`;
@@ -97,9 +104,14 @@ export default function Select({
name,
onChange,
options,
...value && (isMulti ? {
value: Array.isArray(value) ? value.map((val: any) => ({ label: val, value: val })) : []
} : { value: { label: valueLabel, value } }),
...(value &&
(isMulti
? {
value: Array.isArray(value)
? value.map((val: any) => ({ label: val, value: val }))
: [],
}
: { value: { label: valueLabel, value } })),
...rest,
}}
/>
@@ -14,7 +14,8 @@ export enum ModalView { // ignore this modal stuff for now -V
Login,
}
interface ModalProps { // this too
interface ModalProps {
// this too
close: () => void;
open: () => void;
view: ModalView;
@@ -32,7 +33,7 @@ interface SessionContextType {
querySubscriptions?: any;
sessionFetched: boolean;
sessionSubscriptions?: any;
studioAccessCheck: (x:any) => any,
studioAccessCheck: (x: any) => any;
user?: any;
userTokenMatch: (token: string) => boolean;
}
@@ -75,12 +76,18 @@ export default function SessionProvider({
sessionSubscriptions,
sessionWrapper,
}: Props) {
const sessionResponse = sessionWrapper?.sessionStateResponse || { logged_in: false, user: null };
const sessionResponse = sessionWrapper?.sessionStateResponse || {
logged_in: false,
user: null,
};
const { logged_in: loggedIn, user } = sessionResponse;
const [view, viewSet] = useState(ModalView.Closed);
const open = () => viewSet(ModalView.Signup);
const close = () => { viewSet(ModalView.Closed); };
const viewSwitch = () => viewSet(view === ModalView.Signup ? ModalView.Login : ModalView.Signup);
const close = () => {
viewSet(ModalView.Closed);
};
const viewSwitch = () =>
viewSet(view === ModalView.Signup ? ModalView.Login : ModalView.Signup);
const check = () => {
if (user) {
return true;
@@ -90,7 +97,9 @@ export default function SessionProvider({
}
};
const userTokenMatch = (otherUserToken: string) =>
!otherUserToken || !user?.user_token ? false : user.user_token === otherUserToken;
!otherUserToken || !user?.user_token
? false
: user.user_token === otherUserToken;
const canEditTtsModel = (userToken: string) =>
user?.can_delete_other_users_tts_models || userTokenMatch(userToken);
const canEditMediaFile = (userToken: string) =>
@@ -98,11 +107,14 @@ export default function SessionProvider({
const canBanUsers = () => user?.can_ban_users || false;
const canAccessStudio = () => {
const hostnameAllowed = StudioRolloutHostnameAllowed();
const userAllowed = !!user?.can_access_studio || !!user?.maybe_feature_flags.includes("studio");
const userAllowed =
!!user?.can_access_studio ||
!!user?.maybe_feature_flags.includes("studio");
return hostnameAllowed && userAllowed;
}
};
const studioAccessCheck = (content: React.ElementType) => canAccessStudio() ? content : <StudioNotAvailable />;
const studioAccessCheck = (content: React.ElementType) =>
canAccessStudio() ? content : <StudioNotAvailable />;
const modal = { close, open, view };
@@ -138,6 +138,7 @@ import StyleVideo from "./pages/style-video";
import CreateBetaKeyPage from "./pages/beta_key/CreateBetaKeyPage";
import RedeemBetaKeyPage from "./pages/beta_key/RedeemBetaKeyPage";
import RedeemSuccessPage from "./pages/beta_key/RedeemSuccessPage";
import BetaKeysListPage from "./pages/beta_key/BetaKeysListPage";
interface Props {
sessionWrapper: SessionWrapper;
@@ -1139,6 +1140,10 @@ class PageContainer extends React.Component<
<RedeemBetaKeyPage />
</Route>
<Route path="/beta-key/list">
<BetaKeysListPage />
</Route>
{/*
// <Route path="/engine-compositor">
// <EngineCompositor
@@ -26,3 +26,11 @@
border: 4px solid lighten($primary, 12%);
}
}
.status-cell {
width: 80px;
}
.status-header {
width: 80px;
}
@@ -0,0 +1,318 @@
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faCaretDown,
faCaretUp,
faCheck,
faTimes,
} from "@fortawesome/pro-solid-svg-icons";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
useReactTable,
ColumnFiltersState,
SortingState,
Column,
} from "@tanstack/react-table";
import { Container, Panel, TempInput } from "components/common";
import PageHeader from "components/layout/PageHeader";
import {
BetaKey,
ListBetaKeys,
ListBetaKeysResponse,
} from "@storyteller/components/src/api/beta_key/ListBetaKeys";
import "./BetaKey.scss";
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
interface CustomColumnMeta {
className?: string;
headerClassName?: string;
canFilter?: boolean;
}
const Filter = ({ column }: { column: Column<BetaKey, unknown> }) => {
const columnFilterValue = column.getFilterValue();
if (column.id === "maybe_redeemed_at") {
return (
// <TempSelect
// value={(columnFilterValue ?? "") as string}
// onChange={e => column.setFilterValue(e.target.value)}
// className="my-2 py-1"
// options={[
// { value: "", label: "All" },
// { value: "redeemed", label: "Redeemed" },
// { value: "not_redeemed", label: "Not Redeemed" },
// ]}
// />\
null
);
}
return (
<TempInput
type="text"
value={(columnFilterValue ?? "") as string}
onChange={e => column.setFilterValue(e.target.value)}
placeholder={`Search...`}
className="my-2 py-1"
/>
);
};
export default function BetaKeysListPage() {
const [data, setData] = useState<BetaKey[]>([]);
const [loading, setLoading] = useState(true);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [sorting, setSorting] = useState<SortingState>([]);
useEffect(() => {
const fetchData = async () => {
try {
const response: ListBetaKeysResponse = await ListBetaKeys("", {});
if (response.success) {
setData(response.beta_keys);
} else {
console.error("Error fetching data: API call unsuccessful");
}
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const columnHelper = createColumnHelper<BetaKey>();
const columns = [
columnHelper.accessor("maybe_redeemed_at", {
header: "Redeemed?",
cell: info => {
const value = info.getValue();
return value ? (
<FontAwesomeIcon icon={faCheck} className="fs-5 text-success" />
) : (
<FontAwesomeIcon icon={faTimes} className="fs-5 text-danger" />
);
},
meta: {
className: "status-cell",
headerClassName: "status-header",
canFilter: true,
} as CustomColumnMeta,
filterFn: (row, columnId, filterValue) => {
const redeemed = row.getValue(columnId);
if (filterValue === "redeemed") {
return !!redeemed;
}
if (filterValue === "not_redeemed") {
return !redeemed;
}
return true;
},
}),
columnHelper.accessor("key_value", {
header: "Key",
cell: info => {
const key = info.getValue();
if (key) {
return key;
} else {
return "********";
}
},
}),
columnHelper.accessor("maybe_referrer.username", {
header: "Referrer",
cell: info => {
const username = info.getValue();
const userData = info.row.original.maybe_referrer;
const userEmailHash = userData?.gravatar_hash || "";
return username ? (
<div className="d-flex gap-1 align-items-center">
<Gravatar
size={18}
username={username}
email_hash={userEmailHash}
avatarIndex={userData?.default_avatar.image_index}
backgroundIndex={userData?.default_avatar.color_index}
/>
<Link to={`/profile/${username}`}>{username}</Link>
</div>
) : (
"None"
);
},
}),
columnHelper.accessor("maybe_redeemer.username", {
header: "Redeemed by",
cell: info => {
const username = info.getValue();
const userData = info.row.original.maybe_redeemer;
const userEmailHash = userData?.gravatar_hash || "";
return username ? (
<div className="d-flex gap-1 align-items-center">
<Gravatar
size={18}
username={username}
email_hash={userEmailHash}
avatarIndex={userData?.default_avatar.image_index}
backgroundIndex={userData?.default_avatar.color_index}
/>
<Link to={`/profile/${username}`}>{username}</Link>
</div>
) : (
"-"
);
},
}),
columnHelper.accessor("created_at", {
header: "Created Date",
cell: info => new Date(info.getValue()).toLocaleDateString(),
}),
columnHelper.accessor("maybe_redeemed_at", {
header: "Redemption Date",
cell: info => {
const value = info.getValue();
return value ? new Date(value).toLocaleDateString() : "-";
},
}),
columnHelper.accessor("creator.username", {
header: "Key Creator",
cell: info => {
const username = info.getValue();
const userData = info.row.original.creator;
const userEmailHash = userData?.gravatar_hash || "";
return (
<div className="d-flex gap-1 align-items-center">
<Gravatar
size={18}
username={username}
email_hash={userEmailHash}
avatarIndex={userData?.default_avatar.image_index}
backgroundIndex={userData?.default_avatar.color_index}
/>
<Link to={`/profile/${username}`}>{username}</Link>
</div>
);
},
}),
];
const table = useReactTable({
data,
columns,
state: {
columnFilters,
sorting,
},
onColumnFiltersChange: setColumnFilters,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<Container type="panel-full">
<PageHeader title="Beta Keys List" subText="List of beta keys created" />
<Panel padding={true}>
<div>
{loading ? (
<div>Loading...</div>
) : (
<>
<div className="table-responsive">
<table className="table w-100">
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
colSpan={header.colSpan}
style={{ cursor: "pointer" }}
>
{header.isPlaceholder ? null : (
<>
<div
{...{
className: header.column.getCanSort()
? "cursor-pointer select-none"
: "",
onClick:
header.column.getToggleSortingHandler(),
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getIsSorted() ? (
header.column.getIsSorted() === "asc" ? (
<FontAwesomeIcon
icon={faCaretUp}
className="ms-2 opacity-75"
/>
) : (
<FontAwesomeIcon
icon={faCaretDown}
className="ms-2 opacity-75"
/>
)
) : null}
</div>
{header.column.getCanFilter() ? (
<div>
<Filter column={header.column} />
</div>
) : null}
</>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td
key={cell.id}
className={
(cell.column.columnDef.meta as CustomColumnMeta)
?.className
}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{data.length === 0 && (
<div className="mt-5">No data available</div>
)}
</>
)}
</div>
</Panel>
</Container>
);
}
@@ -101,11 +101,10 @@ export default function CreateBetaKeyPage() {
<Panel padding={true}>
<div className="d-flex flex-column gap-3">
<TempInput
label="Username"
placeholder="Username of key owner"
label="Username of Referrer"
placeholder="Username of referrer (optional)"
value={username}
onChange={e => setUsername(e.target.value)}
required={true}
/>
<TempInput
label="Number of Keys"
@@ -12,7 +12,7 @@ export default function RedeemBetaKeyPage() {
const [loading, setLoading] = useState(false);
const [showAlert, setShowAlert] = useState(false);
const history = useHistory();
const { loggedIn } = useSession();
const { user, loggedIn } = useSession();
const handleGoToSuccess = () => {
history.push("/beta-key/redeem/success");
@@ -78,6 +78,8 @@ export default function RedeemBetaKeyPage() {
);
}
console.log(user);
return (
<>
<Container type="panel" className="redeem-container">
+20
View File
@@ -4472,6 +4472,7 @@ __metadata:
resolution: "@storyteller/root@workspace:."
dependencies:
"@react-spring/types": ^9.7.3
"@tanstack/react-table": ^8.17.3
"@typeform/embed": ^4.8.1
"@typeform/embed-react": ^3.15.0
craco: ^0.0.3
@@ -4757,6 +4758,25 @@ __metadata:
languageName: node
linkType: hard
"@tanstack/react-table@npm:^8.17.3":
version: 8.17.3
resolution: "@tanstack/react-table@npm:8.17.3"
dependencies:
"@tanstack/table-core": 8.17.3
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
checksum: ec635f9c6e66bc5655e71ef5bb5087c78fca79e619d8cf80ba395152d88e72ef78e87cbb3e4b9e0b4d86504068c8bf5ba320309529ef9d3dd1ee4630bf75067a
languageName: node
linkType: hard
"@tanstack/table-core@npm:8.17.3":
version: 8.17.3
resolution: "@tanstack/table-core@npm:8.17.3"
checksum: 4f7115d6198f63bea6a00a1c8f33288453463afbede716365b99b3ffa565c46861eb2903c26d3d1de35211ad85fa13bc1e0f35b3546b8f7fc46218441cf06700
languageName: node
linkType: hard
"@testing-library/dom@npm:*":
version: 8.11.3
resolution: "@testing-library/dom@npm:8.11.3"