Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Workshop package #984

Merged
merged 25 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ ExternalProject_Add(
set(JS_DIRS
services/system/Accounts/ui:Accounts_js
services/user/Homepage/ui:Homepage_js
services/user/Workshop/ui:Workshop_js
services/user/Supervisor/ui:Supervisor_js
services/user/Explorer/ui:Explorer_js
services/user/Tokens/ui:Tokens_js
Expand Down Expand Up @@ -730,6 +731,17 @@ psibase_package(
DEPENDS wasm
)

psibase_package(
OUTPUT ${SERVICE_DIR}/Workshop.psi
NAME Workshop
VERSION ${PSIBASE_VERSION}
DESCRIPTION "A dashboard for developers to create and manage apps"
PACKAGE_DEPENDS "HttpServer(^${PSIBASE_VERSION})" "Sites(^${PSIBASE_VERSION})" "Accounts(^${PSIBASE_VERSION})" "CommonApi(^${PSIBASE_VERSION})" "Registry(^${PSIBASE_VERSION})"
SERVICE workshop
DATA GLOB ${CMAKE_CURRENT_SOURCE_DIR}/services/user/Workshop/ui/dist/* /
DEPENDS ${Workshop_js_DEP}
)

psibase_package(
OUTPUT ${SERVICE_DIR}/Fractal.psi
NAME Fractal
Expand Down Expand Up @@ -773,7 +785,7 @@ psibase_package(
DESCRIPTION "All development services"
PACKAGE_DEPENDS Accounts Registry AuthAny AuthSig AuthDelegate Branding Brotli Chainmail ClientData CommonApi CpuLimit
Docs Events Explorer Fractal Invite Nft Packages Producers HttpServer
Sites SetCode StagedTx Supervisor Symbol Tokens Transact Homepage
Sites SetCode StagedTx Supervisor Symbol Tokens Transact Homepage Workshop
)

psibase_package(
Expand All @@ -783,7 +795,7 @@ psibase_package(
DESCRIPTION "All production services"
PACKAGE_DEPENDS Accounts Registry AuthAny AuthSig AuthDelegate Branding Brotli Chainmail ClientData CommonApi CpuLimit
Docs Events Explorer Fractal Invite Nft Packages Producers HttpServer
Sites SetCode StagedTx Supervisor Symbol Tokens Transact Homepage
Sites SetCode StagedTx Supervisor Symbol Tokens Transact Homepage Workshop
)


Expand Down Expand Up @@ -820,7 +832,7 @@ endfunction()
write_package_index(package-index ${SERVICE_DIR}
Accounts AuthAny AuthDelegate AuthSig Branding Brotli Chainmail ClientData CommonApi CpuLimit DevDefault ProdDefault
Docs Events Explorer Fractal Invite Nft Nop Minimal Packages Producers TestDefault HttpServer
Registry Sites SetCode StagedTx Supervisor Symbol TokenUsers Tokens Transact Homepage)
Registry Sites SetCode StagedTx Supervisor Symbol TokenUsers Tokens Transact Homepage Workshop)

install(
FILES ${SERVICE_DIR}/index.json
Expand Down Expand Up @@ -850,6 +862,7 @@ install(
${SERVICE_DIR}/TestDefault.psi
${SERVICE_DIR}/HttpServer.psi
${SERVICE_DIR}/Registry.psi
${SERVICE_DIR}/Workshop.psi
${SERVICE_DIR}/Sites.psi
${SERVICE_DIR}/SetCode.psi
${SERVICE_DIR}/StagedTx.psi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ export const useCurrentAccounts = (enabled: boolean) =>

return res;
},
});
});
2 changes: 1 addition & 1 deletion services/user/Sites/src/Sites.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ namespace SystemService
"default-src 'self';" //
"font-src 'self' https:;" //
"script-src 'self' 'unsafe-eval' 'unsafe-inline' blob: https:;" //
"img-src *;" //
"img-src * data:;" //
"style-src 'self' 'unsafe-inline';" //
"frame-src *;" //
"connect-src * blob:;" //
Expand Down
17 changes: 17 additions & 0 deletions services/user/Workshop/ui/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
28 changes: 28 additions & 0 deletions services/user/Workshop/ui/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";

export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
}
);
12 changes: 12 additions & 0 deletions services/user/Workshop/ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Workshop</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
57 changes: 57 additions & 0 deletions services/user/Workshop/ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@rollup/plugin-alias": "^5.1.1",
"@tanstack/react-query": "^5.52.0",
"axios": "^1.7.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"debounce": "^2.1.0",
"framer-motion": "^11.3.29",
"lucide-react": "^0.429.0",
"next-themes": "^0.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-router-dom": "^6.26.1",
"sonner": "^1.7.2",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/node": "^22.5.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
}
}
6 changes: 6 additions & 0 deletions services/user/Workshop/ui/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
13 changes: 13 additions & 0 deletions services/user/Workshop/ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Workshop } from "./components/Workshop";
import { Nav } from "./components/nav";

function App() {
return (
<div className="w-full mx-auto max-w-screen-lg">
<Nav />
<Workshop />
</div>
);
}

export default App;
162 changes: 162 additions & 0 deletions services/user/Workshop/ui/src/components/Workshop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { useLoggedInUser } from "@/hooks/useLoggedInUser";
import { Button } from "./ui/button";
import { useCreateConnectionToken } from "@/hooks/useCreateConnectionToken";
import { useSetMetadata } from "@/hooks/useSetMetadata";
import {
appMetadataQueryKey,
MetadataResponse,
Status,
useAppMetadata,
} from "@/hooks/useAppMetadata";
import { Switch } from "@/components/ui/switch";
import { MetaDataForm } from "./metadata-form";
import { usePublishApp } from "@/hooks/usePublishApp";
import { queryClient } from "@/main";
import { z } from "zod";
import { toast } from "sonner";
import { Label } from "./ui/label";
import { Spinner } from "./ui/spinner";
import { useBranding } from "@/hooks/useBranding";
import { ErrorCard } from "./error-card";
import { IntroCard } from "./intro-card";

const setStatus = (
metadata: z.infer<typeof MetadataResponse>,
published: boolean
): z.infer<typeof MetadataResponse> => ({
...metadata,
extraMetadata: {
...metadata.extraMetadata,
status: published ? Status.Enum.published : Status.Enum.unpublished,
},
});

const setCacheData = (appName: string, checked: boolean) => {
queryClient.setQueryData(appMetadataQueryKey(appName), (data: unknown) => {
if (data) {
return setStatus(MetadataResponse.parse(data), checked);
}
});
};

export const Workshop = () => {
const {
data: currentUser,
isFetched: isFetchedUser,
error: loggedInUserError,
} = useLoggedInUser();
const {
data: metadata,
isSuccess,
error: metadataError,
} = useAppMetadata(currentUser);

const { data: networkName } = useBranding();

const { mutateAsync: login } = useCreateConnectionToken();
const { mutateAsync: updateMetadata } = useSetMetadata();
const { mutateAsync: publishApp } = usePublishApp();

const handleChecked = async (checked: boolean, appName: string) => {
try {
setCacheData(appName, checked);
toast(`Updating..`, {
description: `${checked ? "Publishing" : "Unpublishing"} ${appName}`,
});
void (await publishApp({ account: appName, publish: checked }));
toast(checked ? "Published" : "Unpublished", {
description: checked
? `${appName} is now published.`
: `${appName} is no longer published.`,
});
} catch (e) {
toast("Failed to update", {
description:
e instanceof Error ? e.message : "Unrecognised error, see logs.",
});
setCacheData(appName, !checked);
}
};

const isAppPublished = metadata
? metadata.extraMetadata.status == Status.Enum.published
: false;

const error = loggedInUserError || metadataError;
const isLoading = !isSuccess;
const isNotLoggedIn = currentUser === null && isFetchedUser;

if (error) {
return <ErrorCard error={error} />;
} else if (isNotLoggedIn) {
return (
<IntroCard
networkName={networkName}
onLogin={() => {
login();
}}
/>
);
} else if (isLoading) {
return (
<div className="h-dvh w-full flex justify-center">
<div className="w-full">
<div className="w-full flex justify-center">
<Spinner size="lg" className="bg-black text-center dark:bg-white" />
</div>
<div>
{isFetchedUser
? "Fetching app metadata..."
: "Fetching account status..."}
</div>
</div>
</div>
);
} else
return (
<div className="mt-4 mx-4">
<div className="flex flex-col gap-4">
<Button
variant="secondary"
onClick={() => {
login();
}}
>
Change app
</Button>
<div className="flex justify-between">
<div className="text-lg font-semibold text-center">
{currentUser}
</div>
{isSuccess && (
<div className="flex gap-2">
<div className="flex flex-col justify-center">
<Label>Publish</Label>
</div>
<Switch
checked={isAppPublished}
disabled={!currentUser}
onCheckedChange={(checked) =>
handleChecked(checked, z.string().parse(currentUser))
}
/>
</div>
)}
</div>

{isSuccess && (
<MetaDataForm
existingValues={metadata ? metadata.appMetadata : undefined}
onSubmit={async (x) => {
await updateMetadata({
...x,
owners: [],
});
return x;
}}
/>
)}
</div>
</div>
);
};
Loading
Loading