OPS - TASKER: capture editable logo link overlay
This commit is contained in:
parent
d3af184096
commit
25d3004cef
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Tasker overlay: editable NODE.DC logo link
|
||||||
|
|
||||||
|
This overlay captures the Synology Tasker source hotfix that routes NODE.DC logo clicks through the Hub public brand config:
|
||||||
|
|
||||||
|
- Hub source of truth: `https://hub.nodedc.ru/api/public/brand`
|
||||||
|
- Editable admin field: Hub admin → Platform → Misc → Logo link
|
||||||
|
- Fallback for `*.nodedc.ru`: `https://hub.nodedc.ru/`
|
||||||
|
|
||||||
|
## Apply to Synology source
|
||||||
|
|
||||||
|
From the platform repo root on a machine that has `/Volumes/docker` mounted:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -av infra/synology/tasker-overlays/logo-link-brand-hotfix/files/ \
|
||||||
|
/Volumes/docker/nodedc-platform/tasker/plane-src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Then deploy web only:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /volume1/docker/nodedc-platform/tasker/plane-src
|
||||||
|
BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 sh rebuild-nas-legacy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Admin image can be rebuilt separately if needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /volume1/docker/nodedc-platform/tasker/plane-src
|
||||||
|
BUILD_BACKEND=0 BUILD_WEB=0 BUILD_ADMIN=1 sh rebuild-nas-legacy.sh
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.yarn
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
### NextJS ###
|
||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# Production
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
.react-router/
|
||||||
|
**/build/
|
||||||
|
**/.react-router/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
.history
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# Turborepo
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
## Django ##
|
||||||
|
venv
|
||||||
|
.venv
|
||||||
|
*.pyc
|
||||||
|
staticfiles
|
||||||
|
mediafiles
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
logs/
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
assets/dist/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
pnpm-debug.log
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
package-lock.json
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Sentry
|
||||||
|
.sentryclirc
|
||||||
|
|
||||||
|
# lock files
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.secrets
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
## packages
|
||||||
|
dist
|
||||||
|
.temp/
|
||||||
|
deploy/selfhost/plane-app/
|
||||||
|
|
||||||
|
## Storybook
|
||||||
|
*storybook.log
|
||||||
|
output.css
|
||||||
|
|
||||||
|
dev-editor
|
||||||
|
# Redis
|
||||||
|
*.rdb
|
||||||
|
*.rdb.gz
|
||||||
|
|
||||||
|
storybook-static
|
||||||
|
|
||||||
|
CLAUDE.md
|
||||||
|
|
||||||
|
build/
|
||||||
|
.react-router/
|
||||||
|
|
||||||
|
build/
|
||||||
|
.react-router/
|
||||||
|
temp/
|
||||||
|
scripts/
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV TURBO_TELEMETRY_DISABLED=1
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH"
|
||||||
|
ENV CI=1
|
||||||
|
|
||||||
|
RUN corepack enable pnpm
|
||||||
|
|
||||||
|
# =========================================================================== #
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
|
||||||
|
RUN pnpm add -g turbo@2.9.4
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create a pruned workspace for just the admin app
|
||||||
|
RUN turbo prune --scope=admin --docker
|
||||||
|
|
||||||
|
# =========================================================================== #
|
||||||
|
|
||||||
|
FROM base AS installer
|
||||||
|
|
||||||
|
# Build in production mode; we still install dev deps explicitly below
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Public envs required at build time (pick up via process.env)
|
||||||
|
ARG VITE_API_BASE_URL=""
|
||||||
|
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
||||||
|
ARG VITE_API_BASE_PATH="/api"
|
||||||
|
ENV VITE_API_BASE_PATH=$VITE_API_BASE_PATH
|
||||||
|
|
||||||
|
ARG VITE_ADMIN_BASE_URL=""
|
||||||
|
ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
|
||||||
|
ARG VITE_ADMIN_BASE_PATH="/nodedcsudo"
|
||||||
|
ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH
|
||||||
|
|
||||||
|
ARG VITE_SPACE_BASE_URL=""
|
||||||
|
ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL
|
||||||
|
ARG VITE_SPACE_BASE_PATH="/spaces"
|
||||||
|
ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH
|
||||||
|
|
||||||
|
ARG VITE_LIVE_BASE_URL=""
|
||||||
|
ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL
|
||||||
|
ARG VITE_LIVE_BASE_PATH="/live"
|
||||||
|
ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH
|
||||||
|
|
||||||
|
ARG VITE_WEB_BASE_URL=""
|
||||||
|
ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL
|
||||||
|
ARG VITE_WEB_BASE_PATH=""
|
||||||
|
ENV VITE_WEB_BASE_PATH=$VITE_WEB_BASE_PATH
|
||||||
|
|
||||||
|
ARG VITE_NODEDC_LAUNCHER_URL=""
|
||||||
|
ENV VITE_NODEDC_LAUNCHER_URL=$VITE_NODEDC_LAUNCHER_URL
|
||||||
|
|
||||||
|
ARG VITE_WEBSITE_URL="https://plane.so"
|
||||||
|
ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL
|
||||||
|
ARG VITE_SUPPORT_EMAIL="support@plane.so"
|
||||||
|
ENV VITE_SUPPORT_EMAIL=$VITE_SUPPORT_EMAIL
|
||||||
|
|
||||||
|
COPY .gitignore .gitignore
|
||||||
|
COPY --from=builder /app/out/json/ .
|
||||||
|
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Copy full directory structure before fetch to ensure all package.json files are available
|
||||||
|
COPY --from=builder /app/out/full/ .
|
||||||
|
COPY turbo.json turbo.json
|
||||||
|
|
||||||
|
# Fetch dependencies to cache store, then install offline with dev deps
|
||||||
|
RUN pnpm fetch --store-dir=/pnpm/store
|
||||||
|
RUN CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
|
||||||
|
|
||||||
|
# Build only the admin package
|
||||||
|
RUN pnpm turbo run build --filter=admin
|
||||||
|
|
||||||
|
# =========================================================================== #
|
||||||
|
|
||||||
|
FROM nginx:1.29-alpine AS production
|
||||||
|
|
||||||
|
COPY apps/admin/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/nodedcsudo
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -fsS http://127.0.0.1:3000/ >/dev/null || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
|
|
||||||
|
export function AuthHeader() {
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
|
||||||
|
<a href={logoLinkUrl}>
|
||||||
|
<span className="tracking-normal text-16 font-semibold text-primary">NODE.DC</span>
|
||||||
|
</a>
|
||||||
|
<span className="rounded-full bg-white/6 px-3 py-1 text-11 font-medium text-secondary">
|
||||||
|
Глобальное администрирование
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Fragment, useEffect, useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { useTheme as useNextTheme } from "next-themes";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { BrainCog, ChevronDown, ExternalLink, Image, LogOut, Mail, Palette, Settings, UserCog2 } from "lucide-react";
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
// plane imports
|
||||||
|
import { API_BASE_URL } from "@plane/constants";
|
||||||
|
import { LockIcon, WorkspaceIcon } from "@plane/propel/icons";
|
||||||
|
import { AuthService } from "@plane/services";
|
||||||
|
import { Avatar } from "@plane/ui";
|
||||||
|
import { cn, getFileURL } from "@plane/utils";
|
||||||
|
// assets
|
||||||
|
import NodeDcLogo from "@/app/assets/logos/nodedc-logo.svg?url";
|
||||||
|
// hooks
|
||||||
|
import { useUser } from "@/hooks/store";
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
|
// local imports
|
||||||
|
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
|
const PRIMARY_NAVIGATION = [
|
||||||
|
{ label: "Основное", href: "/general/", Icon: Settings },
|
||||||
|
{ label: "Почта", href: "/email/", Icon: Mail },
|
||||||
|
{ label: "Аутентификация", href: "/authentication/", Icon: LockIcon },
|
||||||
|
{ label: "Воркспейсы", href: "/workspace/", Icon: WorkspaceIcon },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FEATURE_NAVIGATION = [
|
||||||
|
{ label: "ИИ", href: "/ai/", Icon: BrainCog, description: "OpenAI модель и API-ключ" },
|
||||||
|
{ label: "Изображения", href: "/image/", Icon: Image, description: "Внешние библиотеки изображений" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AdminHeader = observer(function AdminHeader() {
|
||||||
|
const pathName = usePathname();
|
||||||
|
const { currentUser, signOut } = useUser();
|
||||||
|
const { resolvedTheme, setTheme } = useNextTheme();
|
||||||
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
|
const isFeatureRoute = FEATURE_NAVIGATION.some((item) => pathName?.startsWith(item.href));
|
||||||
|
const adminName = currentUser?.display_name || currentUser?.email || "Глобальный админ";
|
||||||
|
const avatarName = currentUser?.display_name || currentUser?.email || "DC";
|
||||||
|
|
||||||
|
const handleThemeSwitch = () => {
|
||||||
|
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||||
|
setTheme(newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (csrfToken === undefined)
|
||||||
|
void authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||||
|
}, [csrfToken]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="nodedc-admin-header relative z-30 flex w-full flex-shrink-0 flex-col gap-4">
|
||||||
|
<div className="nodedc-admin-header-top grid w-full items-center gap-4">
|
||||||
|
<a href={logoLinkUrl} className="nodedc-admin-logo-link inline-flex w-fit items-center" aria-label="NODE.DC">
|
||||||
|
<img src={NodeDcLogo} alt="NODE.DC" className="nodedc-admin-logo" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav className="nodedc-admin-top-nav justify-self-center" aria-label="Основная навигация God Mode">
|
||||||
|
{PRIMARY_NAVIGATION.map((item) => {
|
||||||
|
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
|
||||||
|
return (
|
||||||
|
<Link key={item.href} href={item.href} className="nodedc-admin-top-nav-item" data-active={isActive}>
|
||||||
|
<item.Icon className="size-3.5 stroke-[1.7]" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Menu as="div" className="relative">
|
||||||
|
<Menu.Button className="nodedc-admin-top-nav-item" data-active={isFeatureRoute}>
|
||||||
|
<BrainCog className="size-3.5 stroke-[1.7]" />
|
||||||
|
<span>Возможности</span>
|
||||||
|
<ChevronDown className="size-3 stroke-[2]" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="nodedc-glass-popup-surface absolute top-full left-1/2 z-[120] mt-3 flex w-64 -translate-x-1/2 flex-col gap-1 p-2 outline-none">
|
||||||
|
{FEATURE_NAVIGATION.map((item) => {
|
||||||
|
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
|
||||||
|
return (
|
||||||
|
<Menu.Item key={item.href}>
|
||||||
|
{({ active }) => (
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className={cn("nodedc-admin-feature-menu-item", {
|
||||||
|
"is-active": isActive,
|
||||||
|
"is-hovered": active,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className="grid size-9 place-items-center rounded-full bg-white/6">
|
||||||
|
<item.Icon className="size-4 stroke-[1.7]" />
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className="block truncate text-13 font-medium text-primary">{item.label}</span>
|
||||||
|
<span className="block truncate text-11 text-tertiary">{item.description}</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<a href="/" className="nodedc-admin-top-nav-item">
|
||||||
|
<ExternalLink className="size-3.5 stroke-[1.7]" />
|
||||||
|
<span>В приложение</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Menu as="div" className="relative justify-self-end">
|
||||||
|
<Menu.Button className="nodedc-admin-user-button">
|
||||||
|
<span className="min-w-0 text-right">
|
||||||
|
<span className="block max-w-40 truncate text-14 font-medium text-primary">{adminName}</span>
|
||||||
|
</span>
|
||||||
|
<span className="grid size-10 place-items-center rounded-full bg-white/7">
|
||||||
|
{currentUser ? (
|
||||||
|
<Avatar
|
||||||
|
name={avatarName}
|
||||||
|
src={getFileURL(currentUser.avatar_url)}
|
||||||
|
size={32}
|
||||||
|
shape="circle"
|
||||||
|
className="!text-body-sm-medium"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<UserCog2 className="size-5 text-[rgb(var(--nodedc-card-active-rgb))]" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Menu.Button>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="nodedc-glass-popup-surface absolute top-full right-0 z-[120] mt-3 flex w-60 flex-col divide-y divide-white/6 p-2 text-12 outline-none">
|
||||||
|
<div className="flex flex-col gap-1 px-2 pb-2">
|
||||||
|
<span className="truncate text-13 font-medium text-primary">{adminName}</span>
|
||||||
|
<span className="truncate text-11 text-tertiary">{currentUser?.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="py-2">
|
||||||
|
<Menu.Item as="button" type="button" className="nodedc-admin-menu-action" onClick={handleThemeSwitch}>
|
||||||
|
<Palette className="h-4 w-4 stroke-[1.5]" />
|
||||||
|
{resolvedTheme === "dark" ? "Светлая тема" : "Темная тема"}
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
<div className="py-2">
|
||||||
|
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={signOut}>
|
||||||
|
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||||
|
<Menu.Item as="button" type="submit" className="nodedc-admin-menu-action">
|
||||||
|
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||||
|
Выйти
|
||||||
|
</Menu.Item>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
export function buildNodeDCLauncherUrl(): string {
|
||||||
|
const configuredUrl = process.env.VITE_NODEDC_LAUNCHER_URL;
|
||||||
|
|
||||||
|
if (configuredUrl) {
|
||||||
|
return configuredUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return "http://launcher.local.nodedc/";
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = window.location.hostname.toLowerCase();
|
||||||
|
|
||||||
|
if (hostname.endsWith(".nodedc.ru")) {
|
||||||
|
return "https://hub.nodedc.ru/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostname.endsWith(".nas.nodedc")) {
|
||||||
|
const port = window.location.port ? `:${window.location.port}` : "";
|
||||||
|
return `${window.location.protocol}//launcher.nas.nodedc${port}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "http://launcher.local.nodedc/";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildNodeDCBrandConfigUrl(): string {
|
||||||
|
return new URL("/api/public/brand", buildNodeDCLauncherUrl()).toString();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { buildNodeDCBrandConfigUrl, buildNodeDCLauncherUrl } from "@/helpers/nodedc-brand";
|
||||||
|
|
||||||
|
type TNodeDCBrandPayload = {
|
||||||
|
logoLinkUrl?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useNodeDCBrandLinkUrl() {
|
||||||
|
const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
fetch(buildNodeDCBrandConfigUrl(), { cache: "no-store" })
|
||||||
|
.then((response) => (response.ok ? response.json() : null))
|
||||||
|
.then((payload: TNodeDCBrandPayload | null) => {
|
||||||
|
const configuredUrl = typeof payload?.logoLinkUrl === "string" ? payload.logoLinkUrl.trim() : "";
|
||||||
|
if (isMounted && configuredUrl) setLogoLinkUrl(configuredUrl);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.warn(error instanceof Error ? error.message : "Не удалось загрузить brand config NODE.DC");
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return logoLinkUrl;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { Shapes } from "lucide-react";
|
||||||
|
import { cn } from "@plane/utils";
|
||||||
|
// components
|
||||||
|
import { TopNavPowerK } from "@/components/navigation";
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
|
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||||
|
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||||
|
import { useHome } from "@/hooks/store/use-home";
|
||||||
|
import { ProjectsToolbarMenu } from "./projects-toolbar-menu";
|
||||||
|
import { ExpandedToolbarLink, ExpandedToolbarToolButton, ToolbarNotificationsButton } from "./toolbar-controls";
|
||||||
|
// types
|
||||||
|
import type { TProjectShellToolbarLayoutProps } from "./types";
|
||||||
|
|
||||||
|
export const ExpandedProjectShellToolbarLayout = ({
|
||||||
|
draftsItem,
|
||||||
|
homeItem,
|
||||||
|
isWorkspaceHome,
|
||||||
|
notificationsCount,
|
||||||
|
profileItem,
|
||||||
|
stickiesItem,
|
||||||
|
onOpenNotifications,
|
||||||
|
}: TProjectShellToolbarLayoutProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { toggleWidgetSettings } = useHome();
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("nodedc-expanded-toolbar-shell", { "nodedc-home-top-toolbar": isWorkspaceHome })}>
|
||||||
|
<div className="nodedc-expanded-toolbar">
|
||||||
|
<div className="nodedc-expanded-toolbar-top">
|
||||||
|
<div className="nodedc-expanded-toolbar-left">
|
||||||
|
<a href={logoLinkUrl} className="nodedc-expanded-brand-link" aria-label="NODE.DC">
|
||||||
|
<img src="/nodedc-logo.svg" alt="NODE DC" className="nodedc-expanded-brand-logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nodedc-expanded-toolbar-center">
|
||||||
|
<WorkspaceMenuRoot variant="expanded-toolbar" />
|
||||||
|
<div className="nodedc-expanded-nav-group">
|
||||||
|
<ExpandedToolbarLink item={homeItem} label="Главная" />
|
||||||
|
<ProjectsToolbarMenu variant="expanded" />
|
||||||
|
<ExpandedToolbarLink item={stickiesItem} label="Стикеры" />
|
||||||
|
<ExpandedToolbarLink item={draftsItem} label="Черновики" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nodedc-expanded-toolbar-right">
|
||||||
|
<div className="nodedc-expanded-user-group">
|
||||||
|
<ExpandedToolbarLink item={profileItem} label="Профиль" />
|
||||||
|
<ToolbarNotificationsButton
|
||||||
|
label={t("notification.label")}
|
||||||
|
notificationsCount={notificationsCount}
|
||||||
|
onClick={onOpenNotifications}
|
||||||
|
variant="expanded"
|
||||||
|
/>
|
||||||
|
<UserMenuRoot variant="expanded-toolbar" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nodedc-expanded-toolbar-tools-row">
|
||||||
|
<div className="nodedc-expanded-breadcrumbs-slot" data-nodedc-expanded-breadcrumbs-slot />
|
||||||
|
{!isWorkspaceHome && (
|
||||||
|
<div className="nodedc-expanded-main-tool-cluster">
|
||||||
|
<TopNavPowerK variant="expanded-toolbar" />
|
||||||
|
<div className="nodedc-expanded-header-filters-slot" data-nodedc-expanded-header-filters-slot />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="nodedc-expanded-action-tool-cluster">
|
||||||
|
<div className="nodedc-expanded-tool-slot" data-nodedc-voice-task-toolbar-slot />
|
||||||
|
{isWorkspaceHome && (
|
||||||
|
<ExpandedToolbarToolButton label={t("home.manage_widgets")} onClick={() => toggleWidgetSettings(true)}>
|
||||||
|
<Shapes className="size-4" />
|
||||||
|
</ExpandedToolbarToolButton>
|
||||||
|
)}
|
||||||
|
<div className="nodedc-expanded-primary-action-slot" data-nodedc-expanded-primary-action-slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants";
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { PlaneLockup } from "@plane/propel/icons";
|
||||||
|
import { PageHead } from "@/components/core/page-title";
|
||||||
|
import { EAuthModes } from "@/helpers/authentication.helper";
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
|
import { useInstance } from "@/hooks/store/use-instance";
|
||||||
|
|
||||||
|
const authContentMap = {
|
||||||
|
[EAuthModes.SIGN_IN]: {
|
||||||
|
pageTitle: "auth_actions.sign_up",
|
||||||
|
text: "auth.common.new_to_plane",
|
||||||
|
linkText: "auth_actions.sign_up",
|
||||||
|
linkHref: "/sign-up",
|
||||||
|
},
|
||||||
|
[EAuthModes.SIGN_UP]: {
|
||||||
|
pageTitle: "auth_actions.sign_in",
|
||||||
|
text: "auth.common.already_have_an_account",
|
||||||
|
linkText: "auth_actions.sign_in",
|
||||||
|
linkHref: "/sign-in",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type AuthHeaderProps = {
|
||||||
|
type: EAuthModes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthHeader = observer(function AuthHeader({ type }: AuthHeaderProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
// store
|
||||||
|
const { config } = useInstance();
|
||||||
|
// derived values
|
||||||
|
const enableSignUpConfig = config?.enable_signup ?? false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthHeaderBase
|
||||||
|
pageTitle={t(authContentMap[type].pageTitle)}
|
||||||
|
additionalAction={
|
||||||
|
enableSignUpConfig && (
|
||||||
|
<div className="flex flex-col items-end text-center text-13 font-medium text-tertiary sm:flex-row sm:items-center sm:gap-2">
|
||||||
|
<span className="text-body-sm-regular text-tertiary">{t(authContentMap[type].text)}</span>
|
||||||
|
<Link
|
||||||
|
data-ph-element={AUTH_TRACKER_ELEMENTS.NAVIGATE_TO_SIGN_UP}
|
||||||
|
href={authContentMap[type].linkHref}
|
||||||
|
className="nodedc-auth-link text-body-sm-semibold"
|
||||||
|
>
|
||||||
|
{t(authContentMap[type].linkText)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAuthHeaderBase = {
|
||||||
|
pageTitle: string;
|
||||||
|
additionalAction?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AuthHeaderBase(props: TAuthHeaderBase) {
|
||||||
|
const { pageTitle, additionalAction } = props;
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={pageTitle + " - NODE.DC"} />
|
||||||
|
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6 px-2 py-1">
|
||||||
|
<a href={logoLinkUrl}>
|
||||||
|
<PlaneLockup
|
||||||
|
height={31}
|
||||||
|
width={148}
|
||||||
|
className="nodedc-auth-logo-lockup text-primary transition-opacity hover:opacity-90"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{additionalAction}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { GOD_MODE_URL } from "@plane/constants";
|
||||||
|
// assets
|
||||||
|
import GradientLogo from "@/app/assets/auth/gradient-logo.webp?url";
|
||||||
|
import GradientBgLogo from "@/app/assets/auth/gradient-bg-logo.webp?url";
|
||||||
|
import DefaultLayout from "@/layouts/default-layout";
|
||||||
|
import { PlaneLockup } from "@plane/propel/icons";
|
||||||
|
import { Button } from "@plane/propel/button";
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
|
|
||||||
|
export function InstanceNotReady() {
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaultLayout>
|
||||||
|
<div className="relative z-10 flex h-screen w-screen overflow-hidden">
|
||||||
|
<div className="flex h-full w-full flex-col items-center px-8 pt-6 pb-10">
|
||||||
|
<div className="sticky top-0 flex w-full shrink-0 items-center justify-between gap-6">
|
||||||
|
<a href={logoLinkUrl} aria-label="NODE.DC">
|
||||||
|
<PlaneLockup
|
||||||
|
height={40}
|
||||||
|
width={190}
|
||||||
|
className="nodedc-auth-logo-lockup text-primary transition-opacity hover:opacity-90"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center gap-7">
|
||||||
|
<div className="nodedc-error-shell flex max-w-3xl flex-col items-center gap-11 text-center">
|
||||||
|
<img
|
||||||
|
src={GradientBgLogo}
|
||||||
|
className="pointer-events-none absolute -top-24 -left-32 h-56 w-96 opacity-12"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={GradientBgLogo}
|
||||||
|
className="pointer-events-none absolute -right-20 -bottom-16 h-56 w-96 opacity-12"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<img src={GradientLogo} className="h-24 w-40 object-contain" alt="NODE.DC Logo" />
|
||||||
|
<div className="flex max-w-124 flex-col items-center gap-3">
|
||||||
|
<h1 className="text-h2-semibold text-primary">NODE.DC готов к запуску</h1>
|
||||||
|
<p className="text-center text-body-md-regular text-secondary">
|
||||||
|
Завершите настройку инстанса и создайте первое рабочее пространство, чтобы начать работу с
|
||||||
|
проектами и рабочими элементами.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a href={GOD_MODE_URL} className="w-80">
|
||||||
|
<Button variant="primary" className="nodedc-error-primary w-full" size="xl">
|
||||||
|
Перейти к настройке
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://nodedc.dctouch.ru/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="nodedc-error-link text-13 font-medium"
|
||||||
|
>
|
||||||
|
Служба поддержки
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaultLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { InboxIcon } from "@plane/propel/icons";
|
||||||
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
|
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||||
|
|
||||||
|
type TNodeDCStandaloneShellProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
notificationsCount?: number;
|
||||||
|
onOpenNotifications?: () => void;
|
||||||
|
showUserControls?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeDCStandaloneShell = (props: TNodeDCStandaloneShellProps) => {
|
||||||
|
const { children, notificationsCount = 0, onOpenNotifications, showUserControls = false } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex min-h-screen w-full overflow-hidden bg-[#050507] text-primary">
|
||||||
|
<div className="pointer-events-none absolute inset-0 opacity-80">
|
||||||
|
<div className="absolute top-[-18rem] left-[-12rem] h-[34rem] w-[34rem] rounded-full bg-[rgb(var(--nodedc-accent-rgb))]/10 blur-[120px]" />
|
||||||
|
<div className="absolute right-[-14rem] bottom-[-18rem] h-[38rem] w-[38rem] rounded-full bg-white/7 blur-[140px]" />
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_10%,rgba(255,255,255,0.06),transparent_38%),linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0))]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header className="nodedc-expanded-toolbar-shell absolute inset-x-0 top-0 z-[2]">
|
||||||
|
<div className="nodedc-expanded-toolbar-top">
|
||||||
|
<div className="nodedc-expanded-toolbar-left">
|
||||||
|
<a href={logoLinkUrl} className="nodedc-expanded-brand-link" aria-label="NODE.DC">
|
||||||
|
<img src="/nodedc-logo.svg" alt="NODE DC" className="nodedc-expanded-brand-logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="nodedc-expanded-toolbar-center" />
|
||||||
|
<div className="nodedc-expanded-toolbar-right">
|
||||||
|
{showUserControls && (
|
||||||
|
<div className="nodedc-expanded-user-group">
|
||||||
|
{onOpenNotifications && (
|
||||||
|
<Tooltip tooltipContent={t("notification.label")} position="bottom">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="nodedc-toolbar-icon-button nodedc-expanded-notification-button relative flex items-center justify-center"
|
||||||
|
data-active={false}
|
||||||
|
aria-label={t("notification.label")}
|
||||||
|
onClick={onOpenNotifications}
|
||||||
|
>
|
||||||
|
<span className="nodedc-toolbar-icon-active-dot">
|
||||||
|
<InboxIcon className="size-5" />
|
||||||
|
</span>
|
||||||
|
{notificationsCount > 0 && (
|
||||||
|
<span className="nodedc-toolbar-notification-dot absolute top-1.5 right-1.5 size-2 rounded-full bg-danger-primary" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<UserMenuRoot variant="expanded-toolbar" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="relative z-[1] flex min-h-screen w-full items-center justify-center px-5 py-10 pt-[calc(var(--nodedc-shell-height)+2.25rem)]">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// plane imports
|
||||||
|
import { PlaneLockup } from "@plane/propel/icons";
|
||||||
|
// hooks
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
|
import { useUser } from "@/hooks/store/user";
|
||||||
|
// local imports
|
||||||
|
import { SwitchAccountDropdown } from "./switch-account-dropdown";
|
||||||
|
|
||||||
|
export const OnboardingHeader = observer(function OnboardingHeader() {
|
||||||
|
const { data: user } = useUser();
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
|
const userName = user?.display_name
|
||||||
|
? user.display_name
|
||||||
|
: user?.first_name
|
||||||
|
? `${user.first_name} ${user.last_name ?? ""}`.trim()
|
||||||
|
: user?.email;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sticky top-0 z-10 flex flex-col gap-4">
|
||||||
|
<div className="h-1.5 w-full overflow-hidden rounded-t-lg bg-surface-1">
|
||||||
|
<div className="h-full w-full bg-accent-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-center justify-between gap-6 px-6">
|
||||||
|
<a href={logoLinkUrl} aria-label="NODE.DC">
|
||||||
|
<PlaneLockup height={20} width={95} className="text-primary transition-opacity hover:opacity-90" />
|
||||||
|
</a>
|
||||||
|
<SwitchAccountDropdown fullName={userName} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { buildNodeDCBrandConfigUrl, buildNodeDCLauncherUrl } from "@/helpers/nodedc-auth";
|
||||||
|
|
||||||
|
type TNodeDCBrandPayload = {
|
||||||
|
logoLinkUrl?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useNodeDCBrandLinkUrl() {
|
||||||
|
const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
fetch(buildNodeDCBrandConfigUrl(), { cache: "no-store" })
|
||||||
|
.then((response) => (response.ok ? response.json() : null))
|
||||||
|
.then((payload: TNodeDCBrandPayload | null) => {
|
||||||
|
const configuredUrl = typeof payload?.logoLinkUrl === "string" ? payload.logoLinkUrl.trim() : "";
|
||||||
|
if (isMounted && configuredUrl) setLogoLinkUrl(configuredUrl);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.warn(error instanceof Error ? error.message : "Не удалось загрузить brand config NODE.DC");
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return logoLinkUrl;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DOCKER=/usr/local/bin/docker
|
||||||
|
SRC=/volume1/docker/nodedc-platform/tasker/plane-src
|
||||||
|
APP=/volume1/docker/nodedc-platform/tasker/plane-app
|
||||||
|
BUILD_BACKEND=${BUILD_BACKEND:-1}
|
||||||
|
BUILD_WEB=${BUILD_WEB:-1}
|
||||||
|
BUILD_ADMIN=${BUILD_ADMIN:-0}
|
||||||
|
RECREATE_SERVICES=""
|
||||||
|
|
||||||
|
printf "== sudo session ==\n"
|
||||||
|
sudo -v
|
||||||
|
|
||||||
|
if [ "$BUILD_BACKEND" = "1" ]; then
|
||||||
|
printf "== backend image: nodedc/plane-backend:local ==\n"
|
||||||
|
cd "$SRC/apps/api"
|
||||||
|
sudo DOCKER_BUILDKIT=0 "$DOCKER" build \
|
||||||
|
-t nodedc/plane-backend:local \
|
||||||
|
-f Dockerfile.api .
|
||||||
|
RECREATE_SERVICES="$RECREATE_SERVICES api worker beat-worker"
|
||||||
|
else
|
||||||
|
printf "== skip backend image ==\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$BUILD_WEB" = "1" ]; then
|
||||||
|
printf "== frontend image: nodedc/plane-frontend:ru ==\n"
|
||||||
|
cd "$SRC"
|
||||||
|
sudo DOCKER_BUILDKIT=0 "$DOCKER" build \
|
||||||
|
--build-arg VITE_NODEDC_LAUNCHER_URL=https://hub.nodedc.ru \
|
||||||
|
--build-arg VITE_NODEDC_OIDC_LOGIN_ENABLED=1 \
|
||||||
|
-t nodedc/plane-frontend:ru \
|
||||||
|
-f apps/web/Dockerfile.web.nas-legacy .
|
||||||
|
RECREATE_SERVICES="$RECREATE_SERVICES web"
|
||||||
|
else
|
||||||
|
printf "== skip frontend image ==\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$BUILD_ADMIN" = "1" ]; then
|
||||||
|
printf "== admin image: nodedc/plane-admin:ru ==\n"
|
||||||
|
cd "$SRC"
|
||||||
|
sudo DOCKER_BUILDKIT=1 "$DOCKER" build \
|
||||||
|
--build-arg VITE_NODEDC_LAUNCHER_URL=https://hub.nodedc.ru \
|
||||||
|
-t nodedc/plane-admin:ru \
|
||||||
|
-f apps/admin/Dockerfile.admin .
|
||||||
|
RECREATE_SERVICES="$RECREATE_SERVICES admin"
|
||||||
|
else
|
||||||
|
printf "== skip admin image ==\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$RECREATE_SERVICES" ]; then
|
||||||
|
printf "== nothing to recreate ==\n"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "== recreate tasker services ==\n"
|
||||||
|
cd "$APP"
|
||||||
|
sudo "$DOCKER" compose -p nodedc-tasker \
|
||||||
|
--env-file .env.synology \
|
||||||
|
-f docker-compose.yaml \
|
||||||
|
-f docker-compose.synology.override.yml \
|
||||||
|
up -d --no-build --force-recreate $RECREATE_SERVICES
|
||||||
|
|
||||||
|
printf "== containers ==\n"
|
||||||
|
sudo "$DOCKER" ps \
|
||||||
|
--filter "name=nodedc-tasker" \
|
||||||
|
--format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
|
||||||
|
printf "== smoke: logo ==\n"
|
||||||
|
curl -k -sSI --resolve ops.nodedc.ru:443:127.0.0.1 \
|
||||||
|
https://ops.nodedc.ru/nodedc-logo.svg \
|
||||||
|
| grep -Ei "HTTP/|content-type" || true
|
||||||
|
|
||||||
|
printf "== smoke: websocket ==\n"
|
||||||
|
curl -k -i --http1.1 --resolve ops.nodedc.ru:443:127.0.0.1 \
|
||||||
|
-H "Connection: Upgrade" \
|
||||||
|
-H "Upgrade: websocket" \
|
||||||
|
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||||
|
-H "Sec-WebSocket-Version: 13" \
|
||||||
|
https://ops.nodedc.ru/live/nodedc/stream \
|
||||||
|
--max-time 35 \
|
||||||
|
2>/dev/null | sed -n "1,25p" || true
|
||||||
|
|
||||||
|
printf "== done ==\n"
|
||||||
Loading…
Reference in New Issue