diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0d24b8267..faf08f5bc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -105,19 +105,20 @@ jobs:
container:
name: Container and E2E
- needs: fast-gate
+ needs: [fast-gate, preview-amd64]
if: |
- github.event_name == 'merge_group' ||
- (github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
- (github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
- (github.event_name == 'workflow_dispatch' && (inputs.operation == 'validate' || inputs.operation == 'publish'))
+ !cancelled() && needs.fast-gate.result == 'success' &&
+ (
+ github.event_name == 'merge_group' ||
+ (github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
+ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
+ (github.event_name == 'workflow_dispatch' && (inputs.operation == 'validate' || inputs.operation == 'publish'))
+ )
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
- packages: write
- outputs:
- digest: ${{ steps.preview-build.outputs.digest }}
+ packages: read
steps:
- uses: actions/checkout@v7
if: "!env.ACT"
@@ -132,8 +133,18 @@ jobs:
if: "!env.ACT"
uses: docker/setup-buildx-action@v4
+ - name: Reuse amd64 preview image
+ if: needs.preview-amd64.outputs.digest != '' && !env.ACT
+ env:
+ DIGEST: ${{ needs.preview-amd64.outputs.digest }}
+ run: |
+ echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
+ docker pull "${PREVIEW_IMAGE}@${DIGEST}"
+ docker tag "${PREVIEW_IMAGE}@${DIGEST}" homarr-ci
+ docker tag "${PREVIEW_IMAGE}@${DIGEST}" homarr-e2e
+
- name: Build container once
- if: "!env.ACT"
+ if: needs.preview-amd64.outputs.digest == '' && !env.ACT
uses: docker/build-push-action@v7
with:
context: .
@@ -148,8 +159,8 @@ jobs:
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.ref.name=pr-${{ github.event.pull_request.number || inputs.pr_number || 'none' }}
- cache-from: type=gha,scope=homarr-linux-amd64
- cache-to: type=gha,scope=homarr-linux-amd64,mode=max
+ cache-from: type=gha,scope=homarr-e2e
+ cache-to: type=gha,scope=homarr-e2e,mode=max
secrets: |
TURBO_API=${{ secrets.TURBO_API }}
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
@@ -190,7 +201,10 @@ jobs:
run: pnpm exec playwright install-deps chromium
- name: Run E2E tests
- run: pnpm test:e2e
+ env:
+ NODE_ENV: development
+ CI: "true"
+ run: pnpm exec vitest e2e --retry 1 --maxWorkers 4
- name: Upload E2E artifacts
if: failure() && !env.ACT
@@ -200,29 +214,44 @@ jobs:
path: e2e/test-results/
retention-days: 7
- - name: Log in to GHCR for preview publication
- if: |
- !env.ACT &&
- (
- (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
- (github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
- (github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
- )
+ preview-amd64:
+ name: Build amd64 preview image
+ needs: fast-gate
+ if: |
+ (
+ (github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) ||
+ (github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
+ (github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
+ )
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ contents: read
+ packages: write
+ outputs:
+ digest: ${{ steps.preview-build.outputs.digest }}
+ steps:
+ - uses: actions/checkout@v7
+ if: "!env.ACT"
+ with:
+ persist-credentials: false
+ ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
+
+ - name: Log in to GHCR
+ if: "!env.ACT"
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Push amd64 preview image by digest
+ - name: Set up Docker Buildx
+ if: "!env.ACT"
+ uses: docker/setup-buildx-action@v4
+
+ - name: Build and push amd64 preview image by digest
id: preview-build
- if: |
- !env.ACT &&
- (
- (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
- (github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
- (github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
- )
+ if: "!env.ACT"
uses: docker/build-push-action@v7
with:
context: .
@@ -230,16 +259,13 @@ jobs:
platforms: linux/amd64
outputs: type=image,"name=${{ env.PREVIEW_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=homarr-linux-amd64
+ cache-to: type=gha,scope=homarr-linux-amd64,mode=max
secrets: |
TURBO_API=${{ secrets.TURBO_API }}
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
- - name: Show Act publication result
- if: env.ACT
- run: echo "Act validated the publication path; external GHCR writes are disabled locally."
-
preview-arm64:
name: Build arm64 preview image
needs: fast-gate
@@ -294,7 +320,7 @@ jobs:
publish-preview:
name: Publish multi-platform preview image
- needs: [container, preview-arm64]
+ needs: [preview-amd64, preview-arm64]
if: |
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
@@ -345,7 +371,7 @@ jobs:
- name: Publish preview manifest
if: "!env.ACT"
env:
- AMD64_DIGEST: ${{ needs.container.outputs.digest }}
+ AMD64_DIGEST: ${{ needs.preview-amd64.outputs.digest }}
ARM64_DIGEST: ${{ needs.preview-arm64.outputs.digest }}
run: |
docker buildx imagetools create -t "${PREVIEW_IMAGE}:${PREVIEW_TAG}" \
diff --git a/.github/workflows/skip-renovate-stability-days.yml b/.github/workflows/skip-renovate-stability-days.yml
index e85b2e33e..11f052d0d 100644
--- a/.github/workflows/skip-renovate-stability-days.yml
+++ b/.github/workflows/skip-renovate-stability-days.yml
@@ -1,5 +1,6 @@
permissions:
statuses: write
+ contents: read
on:
pull_request:
@@ -16,12 +17,20 @@ jobs:
env:
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: |
- gh api \
- --method POST \
- -H "Accept: application/vnd.github+json" \
- -H "X-GitHub-Api-Version: 2022-11-28" \
- /repos/${{github.repository}}/statuses/${{github.event.pull_request.head.sha}} \
- -f 'state=success' \
- -f 'target_url=https://docs.renovatebot.com/configuration-options#minimumreleaseage' \
- -f 'description=Skipped for non renovate branches' \
- -f 'context=renovate/stability-days'
+ for attempt in 1 2 3 4 5; do
+ if gh api \
+ --method POST \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ /repos/${{github.repository}}/statuses/${{github.event.pull_request.head.sha}} \
+ -f 'state=success' \
+ -f 'target_url=https://docs.renovatebot.com/configuration-options#minimumreleaseage' \
+ -f 'description=Skipped for non renovate branches' \
+ -f 'context=renovate/stability-days' \
+ --silent; then
+ exit 0
+ fi
+ echo "status POST failed (attempt $attempt/5), retrying in 5s"
+ sleep 5
+ done
+ exit 1
diff --git a/apps/docs/docs/advanced/environment-variables/index.mdx b/apps/docs/docs/advanced/environment-variables/index.mdx
index 140ce3873..4048512a1 100644
--- a/apps/docs/docs/advanced/environment-variables/index.mdx
+++ b/apps/docs/docs/advanced/environment-variables/index.mdx
@@ -58,11 +58,12 @@ A random secret can be generated by using the following command: `openssl rand -
## Docker
-| Environment Variable | Description | Possible values | Default |
-| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------- |
-| `DOCKER_HOSTNAMES` | Comma-separated list of hostnames to connect to | For example `localhost,docker.example.com` | - |
-| `DOCKER_PORTS` | Comma-separated list of ports to connect to | For example `2375,2376` | - |
-| `DOCKER_SOCKET_PATHS` | Comma-separated list of mounted Docker-compatible socket paths. See the [Docker and Podman integration guide](/docs/integrations/docker) | For example `/var/run/docker.sock,/var/run/podman.sock` | - |
+| Environment Variable | Description | Possible values | Default |
+| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------- |
+| `DOCKER_ENDPOINTS` | JSON array of named, capability-scoped Docker or Podman endpoints. Supports socket, explicitly insecure TCP, and TLS transports. See the [Docker and Podman integration guide](/docs/integrations/docker) | A JSON array of endpoint descriptors | - |
+| `DOCKER_HOSTNAMES` | Legacy comma-separated list of plaintext TCP hostnames. Ignored when `DOCKER_ENDPOINTS` is set | For example `localhost,docker.example.com` | - |
+| `DOCKER_PORTS` | Legacy comma-separated list of plaintext TCP ports. Ignored when `DOCKER_ENDPOINTS` is set | For example `2375,2376` | - |
+| `DOCKER_SOCKET_PATHS` | Legacy comma-separated list of mounted Docker-compatible socket paths. Ignored when `DOCKER_ENDPOINTS` is set | For example `/var/run/docker.sock,/var/run/podman.sock` | - |
## Database
diff --git a/apps/docs/docs/integrations/docker/index.mdx b/apps/docs/docs/integrations/docker/index.mdx
index 04ed74fa5..2b83460dc 100644
--- a/apps/docs/docs/integrations/docker/index.mdx
+++ b/apps/docs/docs/integrations/docker/index.mdx
@@ -122,9 +122,77 @@ services:
DOCKER_SOCKET_PATHS: /var/run/docker.sock,/var/run/podman.sock
```
+For named endpoints, transport security, or least-privilege actions, prefer `DOCKER_ENDPOINTS`. It is a JSON array where every endpoint declares its identity, transport, and capabilities:
+
+```yaml
+services:
+ homarr:
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - ./docker-ca.pem:/run/secrets/docker-ca.pem:ro
+ environment:
+ DOCKER_ENDPOINTS: >-
+ [
+ {
+ "id": "local",
+ "name": "Local Docker",
+ "kind": "docker",
+ "transport": { "type": "socket", "path": "/var/run/docker.sock" },
+ "capabilities": ["inventory", "logs", "lifecycle", "remove"]
+ },
+ {
+ "id": "remote-read-only",
+ "name": "Production inventory",
+ "kind": "docker",
+ "transport": {
+ "type": "tls",
+ "host": "docker.example.com",
+ "port": 2376,
+ "caPath": "/run/secrets/docker-ca.pem"
+ },
+ "capabilities": ["inventory", "logs"]
+ }
+ ]
+```
+
+The stable `id` keeps containers from different endpoints distinct. `name` is shown in the interface, and `kind` can be `docker` or `podman`. Every endpoint must include the `inventory` capability; optionally add `logs`, `lifecycle`, and `remove`. Homarr hides or disables actions the endpoint does not allow and also enforces the same capabilities on the server.
+
+Supported transports are:
+
+- `socket`: an absolute in-container `path`
+- `tls`: `host`, `port`, and an absolute `caPath`; add both `certPath` and `keyPath` for mutual TLS
+- `tcp`: `host`, `port`, and the explicit acknowledgement `allowInsecure: true`
+
+:::warning[Plaintext TCP]
+Plaintext Docker TCP can grant unauthenticated control of the Docker host. Prefer a local socket, a least-privilege socket proxy, or TLS. The structured configuration refuses TCP unless `allowInsecure` is explicitly set to `true`.
+:::
+
+When `DOCKER_ENDPOINTS` is set, it takes precedence over the legacy `DOCKER_SOCKET_PATHS`, `DOCKER_HOSTNAMES`, and `DOCKER_PORTS` variables.
+
### Managing your Docker containers within Homarr
Homarr allows you to interact with Docker containers running on your system.
+The **Service setup** section compares the visible containers with your existing Homarr apps and integrations. It keeps every service distinct by Docker endpoint and container ID, including multiple containers that use the same integration type.
+
+Homarr can show a service as:
+
+- **Ready to connect** when its image or name matches a supported integration
+- **New service** when it can be added as an app but no integration is recognized
+- **Already represented** or **Linked** when an app or integration already matches
+- **Address changed** when a service appears to match an integration but its reachable address has changed
+
+Suggested addresses are ranked and explain where they came from. A published address or Docker endpoint host is normally usable from a browser; a container-name address is server-only and requires Homarr to share the container network. Wildcard addresses such as `0.0.0.0` are never offered as browser destinations. You can edit every suggestion before continuing.
+
+:::info[Review before changing]
+Service discovery is read-only. Homarr never creates or changes an app or integration automatically; use the action on an individual suggestion to review and confirm its setup.
+:::
+
+Use the inbox filters to focus on services that need attention or those already represented in Homarr. Refresh runs discovery again. Dismissed suggestions are stored in the current browser using their endpoint-qualified service identity and can be restored from the same panel. If multiple existing items could match, Homarr marks the result as ambiguous and does not choose one automatically.
+
+The health badges are an evidence-based projection across Docker, integration configuration, app representation, and persisted widget links. Authentication, API request success, and widget query execution are labeled **Not observed** because Homarr does not currently retain trustworthy last-run results for these layers. Use the suggested integration test or board action to diagnose them live; **Not observed** is never presented as healthy.
+
+If one Docker endpoint is unavailable, Homarr reports that endpoint while continuing to show services from healthy endpoints. An unavailable endpoint is not treated as an empty Docker installation.
+
Each container row has an action menu with the following options:
- **View logs** - opens a dedicated logs page with a live terminal
diff --git a/apps/docs/docs/integrations/kubernetes/index.mdx b/apps/docs/docs/integrations/kubernetes/index.mdx
index 662874467..7183a9b63 100644
--- a/apps/docs/docs/integrations/kubernetes/index.mdx
+++ b/apps/docs/docs/integrations/kubernetes/index.mdx
@@ -14,6 +14,8 @@ import { kubernetesIntegration } from ".";
Homarr provides a user-friendly Kubernetes dashboard that offers an overview of your cluster’s resources in a clear and structured interface. It allows users to view key Kubernetes objects, including `Pods`, `Services`, `Ingresses`, `Nodes`, `ConfigMaps`, `Secrets`, `Namespaces`, and `Volumes`, as well as basic metrics when the Kubernetes Metrics Server is enabled.
+Docker service discovery does not convert Kubernetes resources into Homarr apps or integrations. Kubernetes remains a separate, read-only operational view, so a cluster resource is never changed or linked automatically.
+
:::warning[Read only]
This integration is read-only, meaning Homarr does not modify or manage cluster resources but serves as an informational tool to enhance visibility into your Kubernetes environment.
:::
@@ -25,7 +27,7 @@ This integration is read-only, meaning Homarr does not modify or manage cluster
Before deploying Homarr in Kubernetes, ensure the following prerequisites are met:
- Kubernetes (Version >= 1.24.0)
-- Metrics Server ([Install from here](https://github.com/kubernetes-sigs/metrics-server))
+- Metrics Server ([install from here](https://github.com/kubernetes-sigs/metrics-server)) is optional. Without it, Homarr still shows cluster inventory, node status, and reserved capacity, and identifies live CPU and memory usage as unavailable.
- RBAC Enabled (Required for Homarr Helm chart)
### Configuring RBAC in Helm Chart
@@ -45,6 +47,25 @@ Enabling RBAC in the Helm chart creates the necessary Kubernetes Role-Based Acce
- ClusterRole & ClusterRoleBinding: Extends the same permissions cluster-wide for additional flexibility.
:::
+### Multiple Kubernetes contexts
+
+When Homarr runs outside a cluster, set `KUBECONFIG` explicitly so Homarr loads the contexts from the referenced kubeconfig file or files. Mount the kubeconfig read-only and set the standard `KUBECONFIG` environment variable to its path inside the Homarr container:
+
+```yaml
+services:
+ homarr:
+ volumes:
+ - ./kubeconfig:/app/config/kubeconfig:ro
+ environment:
+ KUBECONFIG: /app/config/kubeconfig
+```
+
+The Kubernetes management pages include a context selector. The selected context is stored in the page URL, so each React Query cache entry and every API request remains context-specific. Resources with the same namespace and name in two contexts therefore remain separate.
+
+Context status is checked independently. If one context is unavailable, the selector still lists healthy contexts. A context is shown as **Metrics unavailable** when its Kubernetes API is reachable but Metrics Server is not; inventory remains available without live CPU and memory usage.
+
+When Homarr runs inside Kubernetes without `KUBECONFIG`, it keeps using the in-cluster service account as the single default context.
+
### Homarr Dashboard Overview
Once deployed, Homarr provides a dashboard displaying Kubernetes metrics and resource information. The main page includes:
diff --git a/apps/docs/docs/management/api/index.mdx b/apps/docs/docs/management/api/index.mdx
index 82fde7057..bd2ab1363 100644
--- a/apps/docs/docs/management/api/index.mdx
+++ b/apps/docs/docs/management/api/index.mdx
@@ -86,6 +86,30 @@ Board layout responses identify each layout with a `mobile`, `base`, or `custom`
Use the interactive API reference to inspect the exact request body for each endpoint under the `boards` and `settings` tags.
+## Docker action payload migration
+
+Docker lifecycle and removal actions now identify each container with both its Docker endpoint and container ID. This is a
+breaking change for tRPC and MCP clients that previously sent an `ids` array to the start, stop, restart, or remove action.
+
+Replace the old payload:
+
+```json
+{ "ids": ["container-id"] }
+```
+
+with endpoint-qualified targets:
+
+```json
+{
+ "targets": [{ "endpointId": "local", "id": "container-id" }]
+}
+```
+
+Read `endpointId` and `id` from `docker.getContainers` (MCP tool `docker_getContainers`) immediately before the action.
+Pass those pairs unchanged to `docker.startAll`, `docker.stopAll`, `docker.restartAll`, or `docker.removeAll`; their MCP tool
+names use underscores, such as `docker_restartAll`. Endpoint qualification prevents identical container IDs on different
+Docker or Podman endpoints from targeting the wrong service.
+
## Partial integration failures
Multi-integration widget queries keep data from integrations that remain available. Calendar and media-server results include
diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx
index 2d14a6016..cc6a5feb6 100644
--- a/apps/docs/docs/management/boards/index.mdx
+++ b/apps/docs/docs/management/boards/index.mdx
@@ -204,6 +204,15 @@ changes, Homarr asks whether to discard them. If the editor cannot load, the boa
style={{ maxWidth: "100%" }}
/>
+While edit mode is active, **Add board content** opens the same Universal Create surface as the global header action and
+Spotlight. It can add a widget, app, integration, Container, board, Workshop item, or Custom Widget when the
+current user and context allow it. Widget setup offers the main canvas, enabled rails, and existing Containers as
+destinations before the widget is created, so the common flow does not require a second move step.
+
+The widget editor also lists its selected integrations. Users with full access can choose **Edit connection** to test,
+diagnose, and repair an integration without leaving the board. A successful repair refreshes the affected widget; the
+saved integration changes apply everywhere that connection is used.
+
Pointer users can drag from the inert widget area; interactive controls are excluded. Containers use the grab button at their top-left corner.
Widgets can be resized from any edge or corner handle. Activate a Container's chrome with its top-left grip before using
its resize handles.
@@ -218,7 +227,11 @@ During a pointer or touch interaction, Escape cancels the move or resize and res
Invalid, outside, or otherwise forbidden drops are cancelled in the same way.
The **Move / resize item** action in the edit menu remains available for precise numeric input.
-
+
#### Preview
diff --git a/apps/docs/docs/management/integrations/index.mdx b/apps/docs/docs/management/integrations/index.mdx
index f4960560d..a7ccbe1a6 100644
--- a/apps/docs/docs/management/integrations/index.mdx
+++ b/apps/docs/docs/management/integrations/index.mdx
@@ -59,6 +59,22 @@ It is not allowed to create an integration without passing the test.
This is to ensure that you minimize the amount of broken applications on your Homarr instance.
The test and create button will perform a network request to the application and authenticate if required.
+When you connect a service from a board, Homarr keeps you in the same setup flow. After the connection succeeds, it
+confirms whether an application shortcut was linked and offers the widgets that support the new integration. Selecting a
+widget returns to its settings with the new integration already selected.
+
+Users with full access can also test and update a selected integration from **Edit widget → Edit integration** on a
+board. Failed tests keep the editor open and show the same connection diagnostics as the management page. Successful
+changes apply to every app and widget that uses the integration.
+
+When app creation is not permitted, the setup form offers linking an existing accessible app instead of attempting to
+create one.
+
+When the connection unlocks useful widgets that are not on the current board yet, the confirmation also shows up to
+three explainable recommendations. A recommendation is based only on the services you can use and the widget kinds
+already present on that board. You can open the existing widget picker prefiltered to the recommendation, or dismiss
+recommendations for that service on that board.
+
## Managing permissions
On the edit page of an integration, you can manage which users / groups can
diff --git a/apps/docs/static/api/open-api-schema.json b/apps/docs/static/api/open-api-schema.json
index 9ad858459..0b1bdd822 100644
--- a/apps/docs/static/api/open-api-schema.json
+++ b/apps/docs/static/api/open-api-schema.json
@@ -1266,9 +1266,15 @@
"properties": {
"boardId": {
"type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "layoutId": {
+ "type": "string"
}
},
- "required": ["boardId"],
+ "required": ["boardId", "name", "layoutId"],
"additionalProperties": false
}
}
diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json
index 45c46acef..9b8c3305d 100644
--- a/apps/nextjs/package.json
+++ b/apps/nextjs/package.json
@@ -66,6 +66,7 @@
"@mantine/form": "catalog:",
"@mantine/hooks": "catalog:",
"@mantine/modals": "catalog:",
+ "@mantine/notifications": "catalog:",
"@mantine/tiptap": "catalog:",
"@modelcontextprotocol/server": "catalog:",
"@rexa-developer/tiks": "catalog:",
diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
index 9a2d65b36..bfd8d33ec 100644
--- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
@@ -8,6 +8,8 @@ import { getRepresentativeLayoutWidth } from "@homarr/boards/layout-preview";
import { useScopedI18n } from "@homarr/translation/client";
import { BoardAdvancedFocusProvider } from "~/components/board/advanced-focus/context";
+import { BoardEmptyState } from "~/components/board/board-empty-state";
+import { BoardSetupChecklist } from "~/components/board/board-setup-checklist";
import {
getBoardLaneColumnCount,
getInitialBoardLogicalHeight,
@@ -64,6 +66,8 @@ export const ClientBoard = () => {
+
+ {
- const { data: session } = useSession();
const board = useRequiredBoard();
- const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
- const { openModal: openAppSelectModal } = useModalAction(AppSelectModal);
- const { openModal: openIntegrationSelectModal } = useModalAction(IntegrationSelectModal);
- const { addContainer } = useContainerActions();
- const { createItem } = useItemActions();
+ const { addWidget, addApp, connectService, addContainer, canConnectService } = useBoardAddActions();
+ const { openModal } = useModalAction(UniversalCreateModal);
const t = useI18n();
+ const pathname = usePathname();
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const handledInitialAdd = useRef(false);
- const handleSelectItem = () => {
- openItemSelectModal({ boardId: board.id });
- };
+ useEffect(() => {
+ if (handledInitialAdd.current || searchParams.get("add") !== "true") return;
+ handledInitialAdd.current = true;
+ addWidget();
- const handleSelectApp = () => {
- openAppSelectModal({
- onSelect: (app) => {
- createItem({
- kind: "app",
- options: { appId: app.id },
- });
- },
- withCreate: session?.user.permissions.includes("app-create") ?? false,
- });
- };
-
- const handleAddIntegration = () => {
- openIntegrationSelectModal({});
- };
+ const nextSearchParams = new URLSearchParams(searchParams.toString());
+ nextSearchParams.delete("add");
+ const query = nextSearchParams.toString();
+ router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
+ }, [addWidget, board.id, pathname, router, searchParams]);
return (
-
+
+ openModal({
+ boardActions: {
+ widget: addWidget,
+ app: addApp,
+ integration: canConnectService ? connectService : undefined,
+ container: addContainer,
+ },
+ })
+ }
+ >
+
+
);
};
diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
index 633c097dd..397d5274f 100644
--- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
@@ -5,6 +5,7 @@ import dynamic from "next/dynamic";
import { useRouter, useSearchParams } from "next/navigation";
import { Box, Center, Loader, Menu, ScrollArea } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
+import { notifications } from "@mantine/notifications";
import {
IconDeviceFloppy,
IconLayoutBoard,
@@ -98,6 +99,9 @@ const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => {
latestBoardRef.current = board;
const { mutate: saveBoard, isPending } = clientApi.board.saveBoard.useMutation({
+ onMutate(boardToSave) {
+ boardToSave.items.forEach(({ id }) => notifications.hide(`board-item-created:${id}`));
+ },
onSuccess() {
showSuccessNotification({
title: t("notification.success.title"),
diff --git a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx
index fedcefa3b..4fb20c461 100644
--- a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx
@@ -54,7 +54,12 @@ export default async function ManageBoardsPage() {
floatingPrimaryAction={canCreateBoards}
>
{boards.length === 0 && (
-
+
)}
{boards.length > 0 && (
diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/integration-test-connection-error.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/integration-test-connection-error.tsx
index a50625106..42bc8b065 100644
--- a/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/integration-test-connection-error.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/integration-test-connection-error.tsx
@@ -42,7 +42,11 @@ export const IntegrationTestConnectionError = ({ error, url }: IntegrationTestCo
{t.rich("integration.testConnection.error.statusCode.otherDescription", {
statusCode: error.data.statusCode.toString(),
- url: () => {error.data.url},
+ url: () => (
+
+ {error.data.url}
+
+ ),
})}
) : (
@@ -50,7 +54,11 @@ export const IntegrationTestConnectionError = ({ error, url }: IntegrationTestCo
{t.rich("integration.testConnection.error.statusCode.description", {
reason: t(`integration.testConnection.error.statusCode.reason.${error.data.reason}`),
statusCode: error.data.statusCode.toString(),
- url: () => {error.data.url},
+ url: () => (
+
+ {error.data.url}
+
+ ),
})}
)
@@ -66,7 +74,7 @@ export const IntegrationTestConnectionError = ({ error, url }: IntegrationTestCo
{t("integration.testConnection.error.common.cause.title")}
-