Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into develop

This commit is contained in:
Juan Di Toro 2024-03-22 11:43:16 +01:00
commit db91cb9590
9 changed files with 199 additions and 83 deletions

View File

@ -27,7 +27,7 @@ export function DataTable<TData extends BaseRecord, TValue>({
const table = useTable({
columns,
refineCoreProps: {
resource: "files"
resource: "file"
}
})

View File

@ -30,7 +30,7 @@ export const accountProvider: SdkProvider = {
if (ret) {
if (ret instanceof Error) {
return Promise.reject(ret)
return Promise.reject(ret satisfies HttpError)
}
} else {
return Promise.reject();

View File

@ -1,4 +1,4 @@
import type {AuthProvider, UpdatePasswordFormTypes} from "@refinedev/core"
import type {AuthProvider, HttpError, UpdatePasswordFormTypes} from "@refinedev/core"
import type {
AuthActionResponse,
@ -8,10 +8,9 @@ import type {
SuccessNotificationResponse
// @ts-ignore
} from "@refinedev/core/dist/interfaces/bindings/auth"
import {Sdk} from "@lumeweb/portal-sdk";
import {Sdk, AccountError} from "@lumeweb/portal-sdk";
import type {AccountInfoResponse} from "@lumeweb/portal-sdk";
;
export type AuthFormRequest = {
email: string;
@ -54,12 +53,16 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
successCb?: () => void;
}
interface CheckResponseResult extends ResponseResult {
authenticated?: boolean;
}
const handleResponse = (result: ResponseResult): AuthActionResponse => {
if (result.ret) {
if (result.ret instanceof Error) {
if (result.ret instanceof AccountError) {
return {
success: false,
error: result.ret,
error: result.ret satisfies HttpError,
redirectTo: result.redirectToError
}
}
@ -79,6 +82,17 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
}
}
const handleCheckResponse = (result: CheckResponseResult): CheckResponse => {
const response = handleResponse(result);
const success = response.success;
delete response.success;
return {
...response,
authenticated: success
}
}
return {
async login(params: AuthFormRequest): Promise<AuthActionResponse> {
const ret = await sdk.account().login({
@ -105,11 +119,11 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
async check(params?: any): Promise<CheckResponse> {
const ret = await sdk.account().ping();
return handleResponse({ret, redirectToError: "/login", successCb: maybeSetupAuth});
return handleCheckResponse({ret, redirectToError: "/login", successCb: maybeSetupAuth});
},
async onError(error: any): Promise<OnErrorResponse> {
return {logout: true};
return {};
},
async register(params: RegisterFormRequest): Promise<AuthActionResponse> {

View File

@ -1,39 +1,135 @@
import type { DataProvider } from "@refinedev/core";
import { SdkProvider } from "~/data/sdk-provider.js";
import {SdkProvider} from "~/data/sdk-provider.js";
import {S5Client} from "@lumeweb/s5-js";
import {PROTOCOL_S5} from "@lumeweb/portal-sdk";
import {Multihash} from "@lumeweb/libs5/lib/multihash.js";
import {AxiosProgressEvent} from "axios";
import {CID, CID_TYPES, METADATA_TYPES, metadataMagicByte, Unpacker} from "@lumeweb/libs5";
export const fileProvider = {
getList: () => {
console.log("Not implemented");
return Promise.resolve({
data: [],
total: 0,
});
},
getOne: () => {
console.log("Not implemented");
return Promise.resolve({
data: {
id: 1
},
});
},
update: () => {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
create: () => {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
deleteOne: () => {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
getApiUrl: () => "",
async function getIsManifest(s5: S5Client, hash: string): Promise<boolean | number> {
let type: number | null;
try {
const abort = new AbortController();
const resp = s5.downloadData(hash, {
onDownloadProgress: (progressEvent: AxiosProgressEvent) => {
if (progressEvent.loaded >= 10) {
abort.abort();
}
},
httpConfig: {
signal: abort.signal,
},
});
const data = await resp;
const unpacker = Unpacker.fromPacked(Buffer.from(data));
try {
const magic = unpacker.unpackInt();
if (magic !== metadataMagicByte) {
return false;
}
type = unpacker.unpackInt();
if (!type || !Object.values(METADATA_TYPES).includes(type)) {
return false;
}
} catch (e) {
return false;
}
} catch (e) {
return false;
}
switch (type) {
case METADATA_TYPES.DIRECTORY:
return CID_TYPES.DIRECTORY;
case METADATA_TYPES.WEBAPP:
return CID_TYPES.METADATA_WEBAPP;
case METADATA_TYPES.MEDIA:
return CID_TYPES.METADATA_MEDIA;
case METADATA_TYPES.USER_IDENTITY:
return CID_TYPES.USER_IDENTITY;
}
return 0;
}
export interface FileItem {
cid: string;
type: string;
size: number;
mimeType: string;
pinned: string;
}
export const fileProvider: SdkProvider = {
sdk: undefined,
async getList() {
const items: FileItem[] = [];
try {
const s5 = fileProvider.sdk?.protocols().get<S5Client>(PROTOCOL_S5)!.getSdk()!;
const pinList = await s5.accountPins();
for (const pin of pinList!.pins) {
const manifest = await getIsManifest(s5, pin.hash) as number;
if (manifest) {
const mHash = Multihash.fromBase64Url(pin.hash);
items.push({
cid: new CID(manifest, mHash, pin.size).toString(),
type: "manifest",
mimeType: "application/octet-stream",
size: pin.size,
pinned: pin.pinned_at,
});
} else {
items.push({
cid: new CID(CID_TYPES.RAW, Multihash.fromBase64Url(pin.hash), pin.size).toString(),
type: "raw",
mimeType: pin.mime_type,
size: pin.size,
pinned: pin.pinned_at,
});
}
}
} catch (e) {
return Promise.reject(e);
}
return {
data: items,
total: items.length,
};
},
getOne() {
console.log("Not implemented");
return Promise.resolve({
data: {
id: 1
},
});
},
update() {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
create() {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
deleteOne() {
console.log("Not implemented");
return Promise.resolve({
data: {},
});
},
getApiUrl() {
return "";
},
} satisfies SdkProvider;

View File

@ -7,15 +7,16 @@ import type {LinksFunction} from "@remix-run/node";
import '@fontsource-variable/manrope';
import {Refine} from "@refinedev/core";
import routerProvider from "@refinedev/remix-router";
import { notificationProvider } from "~/data/notification-provider";
import {SdkContextProvider} from "~/components/lib/sdk-context";
import { Toaster } from "~/components/ui/toaster";
import {notificationProvider} from "~/data/notification-provider";
import {SdkContextProvider, useSdk} from "~/components/lib/sdk-context";
import {Toaster} from "~/components/ui/toaster";
import {getProviders} from "~/data/providers.js";
import {Sdk} from "@lumeweb/portal-sdk";
import resources from "~/data/resources.js";
import {useMemo} from "react";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
{rel: "stylesheet", href: stylesheet},
];
export function Layout({children}: { children: React.ReactNode }) {
@ -29,7 +30,7 @@ export function Layout({children}: { children: React.ReactNode }) {
</head>
<body>
{children}
<Toaster />
<Toaster/>
<ScrollRestoration/>
<Scripts/>
</body>
@ -37,25 +38,32 @@ export function Layout({children}: { children: React.ReactNode }) {
);
}
export default function App() {
const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL)
const providers = getProviders(sdk);
function App() {
const sdk = useSdk();
const providers = useMemo(() => getProviders(sdk as Sdk), [sdk]);
return (
<Refine
authProvider={providers.auth}
routerProvider={routerProvider}
notificationProvider={notificationProvider}
dataProvider={providers.default}
dataProvider={providers}
resources={resources}
options={{disableTelemetry: true}}
>
<SdkContextProvider sdk={sdk}>
<Outlet/>
</SdkContextProvider>
<Outlet/>
</Refine>
);
}
export default function Root() {
const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL)
return (
<SdkContextProvider sdk={sdk}>
<App/>
</SdkContextProvider>
);
}
export function HydrateFallback() {
return <p>Loading...</p>;
}

View File

@ -313,7 +313,7 @@ const ChangePasswordSchema = z
});
const ChangePasswordForm = () => {
const { mutate: updatePassword } = useUpdatePassword<{ password: string }>();
const { mutate: updatePassword } = useUpdatePassword<UpdatePasswordFormRequest>();
const [form, fields] = useForm({
id: "login",
constraint: getZodConstraint(ChangePasswordSchema),
@ -327,6 +327,7 @@ const ChangePasswordForm = () => {
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
updatePassword({
currentPassword: data.currentPassword.toString(),
password: data.newPassword.toString(),
});
},

View File

@ -4,15 +4,8 @@ import { FileIcon, MoreIcon } from "~/components/icons";
import { Checkbox } from "~/components/ui/checkbox";
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "~/components/ui/dropdown-menu";
import { cn } from "~/utils";
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type File = {
name: string;
cid: string;
size: string;
createdOn: string;
};
import {FileItem} from "~/data/file-provider.js";
import {format} from "date-fns/fp";
declare module '@tanstack/table-core' {
interface TableMeta<TData extends RowData> {
@ -20,13 +13,13 @@ declare module '@tanstack/table-core' {
}
}
export const columns: ColumnDef<File>[] = [
export const columns: ColumnDef<FileItem>[] = [
{
id: "select",
size: 20,
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
@ -64,12 +57,12 @@ export const columns: ColumnDef<File>[] = [
header: "Size",
},
{
accessorKey: "createdOn",
accessorKey: "pinned",
size: 200,
header: "Created On",
header: "Pinned On",
cell: ({ row }) => (
<div className="flex items-center justify-between">
{row.getValue("createdOn")}
{format(row.getValue("pinned")) as unknown as string}
<DropdownMenu>
<DropdownMenuTrigger className={
cn("hidden group-hover:block data-[state=open]:block", row.getIsSelected() && "block")
@ -93,4 +86,4 @@ export const columns: ColumnDef<File>[] = [
</div>
)
}
];
];

View File

@ -1,14 +1,16 @@
import { GeneralLayout } from "~/components/general-layout";
import { FileCard, FileCardList, FileTypes } from "~/components/file-card";
import { DataTable } from "~/components/data-table";
import { columns } from "./columns";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { AddIcon } from "~/components/icons";
import {GeneralLayout} from "~/components/general-layout";
import {FileCard, FileCardList, FileTypes} from "~/components/file-card";
import {DataTable} from "~/components/data-table";
import {columns} from "./columns";
import {Input} from "~/components/ui/input";
import {Button} from "~/components/ui/button";
import {AddIcon} from "~/components/icons";
import {Authenticated} from "@refinedev/core";
export default function FileManager() {
return (
<GeneralLayout>
<Authenticated key="dashboard" v3LegacyAuthProviderCompatible>
<GeneralLayout>
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
<FileCardList>
<FileCard
@ -57,5 +59,6 @@ export default function FileManager() {
columns={columns}
/>
</GeneralLayout>
</Authenticated>
);
}

View File

@ -16,7 +16,7 @@
"@conform-to/react": "^1.0.2",
"@conform-to/zod": "^1.0.2",
"@fontsource-variable/manrope": "^5.0.19",
"@lumeweb/portal-sdk": "0.0.0-20240320165911",
"@lumeweb/portal-sdk": "0.0.0-20240321203634",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
@ -42,6 +42,7 @@
"@visx/visx": "^3.10.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.1",