diff --git a/.env.example b/.env.example index c01132523..542478b67 100644 --- a/.env.example +++ b/.env.example @@ -72,14 +72,10 @@ UNSAFE_ENABLE_MOCK_INTEGRATION=true # NEXT_PUBLIC_WORKSHOP_API_URL=http://localhost:8090 # WORKSHOP_API_URL=http://localhost:8090 -# Workshop development service and GitHub OAuth. -# The GitHub OAuth callback URL is http://localhost:8090/api/oauth2-redirect. -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -PB_SUPERUSER_EMAIL= -PB_SUPERUSER_PASSWORD= +# Workshop development ports. PocketBase superusers and GitHub OAuth are +# configured once through the persistent PocketBase dashboard, not environment variables. PB_EXPOSE_PORT=8090 -PB_PUBLIC_URL=http://localhost:8090 +WORKSHOP_WEB_PORT=3003 # Workshop is intentionally consumable by arbitrary self-hosted Homarr origins. # PocketBase is stateless and authorizes writes with bearer tokens, not cookies. PB_ALLOWED_ORIGINS=* diff --git a/.github/workflows/deployment-workshop.yml b/.github/workflows/deployment-workshop.yml deleted file mode 100644 index 9e6328de4..000000000 --- a/.github/workflows/deployment-workshop.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: "[Deployment] Workshop" - -on: - push: - branches: ["dev"] - paths: - - "apps/docs/**" - - "apps/workshop/**" - - "packages/definitions/src/docs/**" - - "packages/validation/**" - - "packages/workshop/**" - - "patches/**" - - "package.json" - - "pnpm-lock.yaml" - - "pnpm-workspace.yaml" - - "turbo.json" - - ".github/workflows/deployment-workshop.yml" - workflow_dispatch: - -permissions: - contents: read - packages: write - -concurrency: - group: workshop-${{ github.ref }} - cancel-in-progress: true - -jobs: - publish: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v7 - - name: Log in to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - name: Build and publish Workshop - uses: docker/build-push-action@v7 - with: - context: . - file: apps/workshop/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ghcr.io/homarr-labs/workshop:dev - ghcr.io/homarr-labs/workshop:sha-${{ github.sha }} - cache-from: type=gha,scope=workshop - cache-to: type=gha,mode=max,scope=workshop diff --git a/.github/workflows/workshop.yml b/.github/workflows/workshop.yml new file mode 100644 index 000000000..22405f7a2 --- /dev/null +++ b/.github/workflows/workshop.yml @@ -0,0 +1,50 @@ +name: Workshop + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: &workshop-paths + - "apps/workshop/**" + - "apps/docs/docusaurus.config.ts" + - "apps/docs/package.json" + - "apps/docs/src/components/workshop/**" + - "apps/docs/src/pages/workshop/**" + - "apps/docs/src/theme/Root.tsx" + - "apps/docs/static/workshop-*" + - "apps/docs/docs/management/workshop/**" + - "packages/workshop/**" + - "packages/validation/**" + - "packages/definitions/**" + - "patches/**" + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - "turbo.json" + - ".github/workflows/workshop.yml" + push: + branches: [dev] + paths: *workshop-paths + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: workshop-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + production-preview: + name: Production preview + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + - name: Setup + uses: ./tooling/github/setup + - name: Copy environment defaults + run: cp .env.example .env + - name: Test Workshop backend + run: pnpm test:workshop-backend + - name: Build and smoke-test Workshop + run: pnpm test:workshop-preview diff --git a/apps/docs/docs/management/custom-widgets/index.mdx b/apps/docs/docs/management/custom-widgets/index.mdx index c33e00876..8d20cbc12 100644 --- a/apps/docs/docs/management/custom-widgets/index.mdx +++ b/apps/docs/docs/management/custom-widgets/index.mdx @@ -84,7 +84,7 @@ The import format uses `"$schema": "homarr-custom-widget-v2"` to identify the wi ## Community Workshop -Use **Install widget** on **Management → Workshop** to inspect and import community widgets. Use **Share in Workshop** from a widget's action menu to publish its canonical export without manually copying JSON. Secrets are never included in exports and must be configured on each Homarr instance. +Use **Install widget** on **Management → Workshop** to inspect and import community widgets. To publish your own widget, export it from Homarr and upload the JSON file in the Workshop submission form. Secrets are never included in exports and must be configured on each Homarr instance. ## Available Mantine Components for Custom JSX diff --git a/apps/docs/docs/management/workshop/index.mdx b/apps/docs/docs/management/workshop/index.mdx index 0f33e13c2..f853cd89d 100644 --- a/apps/docs/docs/management/workshop/index.mdx +++ b/apps/docs/docs/management/workshop/index.mdx @@ -29,7 +29,7 @@ The public site also caches pages you have visited. If your network drops, a pre ## Share a creation -Use **Share in Workshop** beside a custom widget or in a board's custom CSS settings. Sign in with GitHub, provide a public title and description, inspect the exported source, and publish it. You can also publish from the Workshop website by pasting an exported widget or CSS source. +Export a custom widget from Homarr, then choose or drop the JSON file in the Workshop submission form. CSS themes can be pasted into the same form. Sign in with GitHub, provide a public title and description, inspect the source, and publish it. Submissions are public immediately. Authors can update or delete their own submissions. Updating content increases its revision and shows the supplied changelog. @@ -39,6 +39,8 @@ Signed-in members can vote to help useful creations rank higher. Report maliciou ## Accounts and moderation -Workshop accounts use GitHub OAuth and are separate from users in a self-hosted Homarr instance. Workshop moderators can remove submissions, prevent an account from publishing, disable an account, and resolve reports. These actions require a reason and are recorded. +Workshop accounts use GitHub OAuth and are separate from users in a self-hosted Homarr instance. PocketBase creates OAuth accounts directly; staff privileges come only from the server-managed `workshop_staff` collection. Workshop moderators can remove submissions, disable an account, and resolve reports. These actions require a reason and are recorded. The Workshop is a central service operated by Homarr at `homarr.dev`. The documentation website and Workshop API ship in one image and use the same origin. Running it locally is supported for contributors developing or testing this feature, not as a private production marketplace. + +The service image is built directly from the Homarr repository rather than published separately. Contributors can run `pnpm dev:workshop` to start PocketBase in Docker while Docusaurus and the Workshop contracts watch on the host. Use `pnpm preview:workshop` to build the combined production image, or `pnpm reset:workshop` to explicitly clear the gitignored local PocketBase database. Superusers and GitHub OAuth are configured once in the persistent dashboard instead of being passed as environment variables. diff --git a/apps/docs/package.json b/apps/docs/package.json index 50b9596cc..da89ad341 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "scripts": { "docusaurus": "docusaurus", - "dev": "docusaurus start --no-open --port 3003", + "dev": "docusaurus start --no-open --host 0.0.0.0 --port 3003", "start": "docusaurus start --port 3003", "build": "docusaurus build", "serve": "docusaurus serve", diff --git a/apps/docs/src/components/workshop/WorkshopAdmin.tsx b/apps/docs/src/components/workshop/WorkshopAdmin.tsx index fb06f05fe..732da5937 100644 --- a/apps/docs/src/components/workshop/WorkshopAdmin.tsx +++ b/apps/docs/src/components/workshop/WorkshopAdmin.tsx @@ -195,17 +195,6 @@ function WorkshopAdminContent() { "Current account" ) : (
- diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx index 7884f714d..bf8d48a8e 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx @@ -6,7 +6,6 @@ import { IconCopy, IconDots, IconDownload, - IconShare3, IconToggleLeft, IconToggleRight, IconTrash, @@ -20,8 +19,6 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica import { useScopedI18n } from "@homarr/translation/client"; import { MobileAffixButton } from "~/components/manage/mobile-affix-button"; -import { PublishToWorkshopButton } from "~/components/workshop/workshop-browser"; - const iconProps = { size: 16, stroke: 1.5 }; interface WidgetRef { @@ -32,7 +29,6 @@ interface WidgetRef { export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { const t = useScopedI18n("customWidget"); - const workshopT = useScopedI18n("workshop"); const { openConfirmModal } = useConfirmModal(); const deleteMutation = clientApi.customWidget.delete.useMutation(); const duplicateMutation = clientApi.customWidget.duplicate.useMutation(); @@ -144,16 +140,6 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { void handleExport()} leftSection={}> {t("action.export")} - JSON.stringify(await utils.customWidget.export.fetch({ id: widget.id }), null, 2)} - renderTrigger={(open) => ( - }> - {workshopT("action.share")} - - )} - /> ); } - -export function PublishToWorkshopButton({ - type, - getContent, - defaultTitle, - compact = false, - renderTrigger, -}: { - type: WorkshopSubmissionType; - getContent: () => Promise | string; - defaultTitle: string; - compact?: boolean; - renderTrigger?: (open: () => void) => ReactNode; -}) { - const t = useScopedI18n("workshop"); - const client = useMemo(() => new WorkshopClient(apiUrl), []); - const [opened, { open, close }] = useDisclosure(false); - const [title, setTitle] = useState(defaultTitle); - const [description, setDescription] = useState(""); - const [changelog, setChangelog] = useState(""); - useEffect(() => setTitle(defaultTitle), [defaultTitle]); - const publishMutation = useMutation({ - mutationKey: ["workshop", "publish", type], - mutationFn: async () => { - if (!client.currentUser) await client.signInWithGitHub(); - return client.create({ type, title, description, changelog, content: await getContent() }); - }, - onSuccess: close, - }); - return ( - <> - {renderTrigger ? ( - renderTrigger(open) - ) : ( - - )} - { - close(); - publishMutation.reset(); - }} - title={t("publish.title")} - > - - setTitle(event.currentTarget.value)} - /> -