UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: дизайн и русификация God Mode

This commit is contained in:
DCCONSTRUCTIONS
2026-04-29 00:38:04 +03:00
parent 5d336039ba
commit 7f47b85c36
57 changed files with 1104 additions and 588 deletions
@@ -41,17 +41,17 @@ export function InstanceAIForm(props: IInstanceAIForm) {
{
key: "LLM_MODEL",
type: "text",
label: "LLM Model",
label: "LLM-модель",
description: (
<>
Choose an OpenAI engine.{" "}
Выберите модель OpenAI.{" "}
<a
href="https://platform.openai.com/docs/models/overview"
target="_blank"
className="text-accent-primary hover:underline"
rel="noreferrer"
>
Learn more
Подробнее
</a>
</>
),
@@ -62,17 +62,17 @@ export function InstanceAIForm(props: IInstanceAIForm) {
{
key: "LLM_API_KEY",
type: "password",
label: "API key",
label: "API-ключ",
description: (
<>
You will find your API key{" "}
API-ключ находится{" "}
<a
href="https://platform.openai.com/api-keys"
target="_blank"
className="text-accent-primary hover:underline"
rel="noreferrer"
>
here.
здесь.
</a>
</>
),
@@ -89,8 +89,8 @@ export function InstanceAIForm(props: IInstanceAIForm) {
.then(() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success",
message: "AI Settings updated successfully",
title: "Сохранено",
message: "ИИ-настройки обновлены",
})
)
.catch((err) => console.error(err));
@@ -101,7 +101,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
<div className="space-y-3">
<div>
<div className="pb-1 text-18 font-medium text-primary">OpenAI</div>
<div className="text-13 font-regular text-tertiary">If you use ChatGPT, this is for you.</div>
<div className="text-13 font-regular text-tertiary">Используется для встроенных функций на базе OpenAI.</div>
</div>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-12 gap-y-8 lg:grid-cols-3">
{aiFormFields.map((field) => (
@@ -122,15 +122,15 @@ export function InstanceAIForm(props: IInstanceAIForm) {
<div className="flex flex-col items-start gap-4">
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving" : "Save changes"}
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
</Button>
<div className="relative inline-flex items-center gap-1.5 rounded-sm border border-accent-subtle bg-accent-subtle px-4 py-2 text-caption-sm-regular text-accent-secondary">
<Lightbulb className="size-4" />
<div className="nodedc-settings-note relative inline-flex max-w-2xl items-center gap-2 px-4 py-3 text-12 font-regular">
<Lightbulb className="size-4 shrink-0 text-tertiary" />
<div>
If you have a preferred AI models vendor, please get in{" "}
Если нужен другой провайдер ИИ-моделей, свяжитесь{" "}
<a className="font-medium underline" href="https://plane.so/contact">
touch with us.
с командой поддержки.
</a>
</div>
</div>
@@ -25,8 +25,8 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
return (
<PageWrapper
header={{
title: "AI features for all your workspaces",
description: "Configure your AI API credentials so Plane AI features are turned on for all your workspaces.",
title: "ИИ-функции для всех воркспейсов",
description: "Настройте API-ключ и модель, чтобы включить ИИ-возможности во всех рабочих пространствах.",
}}
>
{formattedConfig ? (
@@ -45,6 +45,6 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
);
});
export const meta: Route.MetaFunction = () => [{ title: "Artificial Intelligence Settings - God Mode" }];
export const meta: Route.MetaFunction = () => [{ title: "ИИ-настройки - NODE.DC" }];
export default InstanceAIPage;
@@ -58,9 +58,9 @@ export function InstanceGiteaConfigForm(props: Props) {
{
key: "GITEA_HOST",
type: "text",
label: "Gitea Host",
label: "Хост Gitea",
description: (
<>Use the URL of your Gitea instance. For the official Gitea instance, use &quot;https://gitea.com&quot;.</>
<>Укажите URL вашего Gitea-инстанса. Для официального сервиса используйте &quot;https://gitea.com&quot;.</>
),
placeholder: "https://gitea.com",
error: Boolean(errors.GITEA_HOST),
@@ -72,7 +72,7 @@ export function InstanceGiteaConfigForm(props: Props) {
label: "Client ID",
description: (
<>
You will get this from your{" "}
Возьмите значение в настройках{" "}
<a
tabIndex={-1}
href="https://gitea.com/user/settings/applications"
@@ -80,7 +80,7 @@ export function InstanceGiteaConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
Gitea OAuth application settings.
Gitea OAuth-приложения.
</a>
</>
),
@@ -94,7 +94,7 @@ export function InstanceGiteaConfigForm(props: Props) {
label: "Client secret",
description: (
<>
Your client secret is also found in your{" "}
Секрет клиента находится в настройках{" "}
<a
tabIndex={-1}
href="https://gitea.com/user/settings/applications"
@@ -102,7 +102,7 @@ export function InstanceGiteaConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
Gitea OAuth application settings.
Gitea OAuth-приложения.
</a>
</>
),
@@ -124,8 +124,8 @@ export function InstanceGiteaConfigForm(props: Props) {
url: `${originURL}/auth/gitea/callback/`,
description: (
<>
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
field{" "}
Значение сформировано автоматически. Вставьте его в поле{" "}
<CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
<a
tabIndex={-1}
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
@@ -133,7 +133,7 @@ export function InstanceGiteaConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
here.
здесь.
</a>
</>
),
@@ -147,8 +147,8 @@ export function InstanceGiteaConfigForm(props: Props) {
const response = await updateInstanceConfigurations(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your Gitea authentication is configured. You should test it now.",
title: "Готово",
message: "Аутентификация через Gitea настроена. Проверьте вход перед включением в проде.",
});
reset({
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
@@ -178,7 +178,7 @@ export function InstanceGiteaConfigForm(props: Props) {
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="pt-2.5 text-18 font-medium">Gitea-provided details for Plane</div>
<div className="pt-2.5 text-18 font-medium">Данные Gitea для NODE.DC</div>
{GITEA_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
@@ -202,17 +202,17 @@ export function InstanceGiteaConfigForm(props: Props) {
loading={isSubmitting}
disabled={!isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
</Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
Go back
Назад
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 pt-1.5 pb-4">
<div className="pt-2 text-18 font-medium">Plane-provided details for Gitea</div>
<div className="nodedc-settings-helper-card flex flex-col gap-y-4 px-6 pt-1.5 pb-4">
<div className="pt-2 text-18 font-medium">Данные NODE.DC для Gitea</div>
{GITEA_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -41,14 +41,14 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration",
loading: "Сохранение конфигурации",
success: {
title: "Configuration saved",
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
title: "Конфигурация сохранена",
message: () => `Вход через Gitea ${value === "1" ? "включен" : "отключен"}.`,
},
error: {
title: "Error",
message: () => "Failed to save configuration",
title: "Ошибка",
message: () => "Не удалось сохранить конфигурацию",
},
});
@@ -69,7 +69,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
customHeader={
<AuthenticationMethodCard
name="Gitea"
description="Allow members to login or sign up to plane with their Gitea accounts."
description="Вход и регистрация пользователей через аккаунты Gitea."
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
config={
<ToggleSwitch
@@ -100,6 +100,6 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
</PageWrapper>
);
});
export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }];
export const meta: Route.MetaFunction = () => [{ title: "Gitea OAuth - NODE.DC" }];
export default InstanceGiteaAuthenticationPage;
@@ -62,7 +62,7 @@ export function InstanceGithubConfigForm(props: Props) {
label: "Client ID",
description: (
<>
You will get this from your{" "}
Возьмите значение в настройках{" "}
<a
tabIndex={-1}
href="https://github.com/settings/applications/new"
@@ -70,7 +70,7 @@ export function InstanceGithubConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
GitHub OAuth application settings.
GitHub OAuth-приложения.
</a>
</>
),
@@ -84,7 +84,7 @@ export function InstanceGithubConfigForm(props: Props) {
label: "Client secret",
description: (
<>
Your client secret is also found in your{" "}
Секрет клиента находится в настройках{" "}
<a
tabIndex={-1}
href="https://github.com/settings/applications/new"
@@ -92,7 +92,7 @@ export function InstanceGithubConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
GitHub OAuth application settings.
GitHub OAuth-приложения.
</a>
</>
),
@@ -103,8 +103,8 @@ export function InstanceGithubConfigForm(props: Props) {
{
key: "GITHUB_ORGANIZATION_ID",
type: "text",
label: "Organization ID",
description: <>The organization github ID.</>,
label: "ID организации",
description: <>ID организации GitHub.</>,
placeholder: "123456789",
error: Boolean(errors.GITHUB_ORGANIZATION_ID),
required: false,
@@ -123,7 +123,8 @@ export function InstanceGithubConfigForm(props: Props) {
url: originURL,
description: (
<>
We will auto-generate this. Paste this into the <CodeBlock darkerShade>Authorized origin URL</CodeBlock> field{" "}
Значение сформировано автоматически. Вставьте его в поле{" "}
<CodeBlock darkerShade>Authorized origin URL</CodeBlock>{" "}
<a
tabIndex={-1}
href="https://github.com/settings/applications/new"
@@ -131,7 +132,7 @@ export function InstanceGithubConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
here.
здесь.
</a>
</>
),
@@ -145,8 +146,8 @@ export function InstanceGithubConfigForm(props: Props) {
url: `${originURL}/auth/github/callback/`,
description: (
<>
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
field{" "}
Значение сформировано автоматически. Вставьте его в поле{" "}
<CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
<a
tabIndex={-1}
href="https://github.com/settings/applications/new"
@@ -154,7 +155,7 @@ export function InstanceGithubConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
here.
здесь.
</a>
</>
),
@@ -168,8 +169,8 @@ export function InstanceGithubConfigForm(props: Props) {
const response = await updateInstanceConfigurations(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your GitHub authentication is configured. You should test it now.",
title: "Готово",
message: "Аутентификация через GitHub настроена. Проверьте вход перед включением в проде.",
});
reset({
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
@@ -199,7 +200,7 @@ export function InstanceGithubConfigForm(props: Props) {
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="pt-2.5 text-18 font-medium">GitHub-provided details for Plane</div>
<div className="pt-2.5 text-18 font-medium">Данные GitHub для NODE.DC</div>
{GITHUB_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
@@ -223,32 +224,32 @@ export function InstanceGithubConfigForm(props: Props) {
loading={isSubmitting}
disabled={!isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
</Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
Go back
Назад
</Link>
</div>
</div>
</div>
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
<div className="pt-2 text-18 font-medium">Plane-provided details for GitHub</div>
<div className="pt-2 text-18 font-medium">Данные NODE.DC для GitHub</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
<div className="nodedc-settings-helper-card flex flex-col gap-y-4 px-6 py-4">
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
{/* web service details */}
<div className="flex flex-col overflow-hidden rounded-lg">
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
<div className="nodedc-settings-helper-card flex flex-col overflow-hidden">
<div className="nodedc-settings-helper-card-header flex items-center gap-x-3 px-6 py-3 text-11 font-medium uppercase">
<Monitor className="h-3 w-3" />
Web
Веб
</div>
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
<div className="flex flex-col gap-y-4 px-6 py-4">
{GITHUB_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -49,14 +49,14 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration",
loading: "Сохранение конфигурации",
success: {
title: "Configuration saved",
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
title: "Конфигурация сохранена",
message: () => `Вход через GitHub ${value === "1" ? "включен" : "отключен"}.`,
},
error: {
title: "Error",
message: () => "Failed to save configuration",
title: "Ошибка",
message: () => "Не удалось сохранить конфигурацию",
},
});
@@ -77,7 +77,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
customHeader={
<AuthenticationMethodCard
name="GitHub"
description="Allow members to login or sign up to plane with their GitHub accounts."
description="Вход и регистрация пользователей через аккаунты GitHub."
icon={
<img
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
@@ -116,6 +116,6 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
);
});
export const meta: Route.MetaFunction = () => [{ title: "GitHub Authentication - God Mode" }];
export const meta: Route.MetaFunction = () => [{ title: "GitHub OAuth - NODE.DC" }];
export default InstanceGithubAuthenticationPage;
@@ -58,10 +58,10 @@ export function InstanceGitlabConfigForm(props: Props) {
{
key: "GITLAB_HOST",
type: "text",
label: "Host",
label: "Хост",
description: (
<>
This is either https://gitlab.com or the <CodeBlock>domain.tld</CodeBlock> where you host GitLab.
Укажите https://gitlab.com или <CodeBlock>domain.tld</CodeBlock>, если GitLab развернут у вас.
</>
),
placeholder: "https://gitlab.com",
@@ -74,7 +74,7 @@ export function InstanceGitlabConfigForm(props: Props) {
label: "Application ID",
description: (
<>
Get this from your{" "}
Возьмите значение в настройках{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
@@ -82,7 +82,7 @@ export function InstanceGitlabConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
GitLab OAuth application settings
GitLab OAuth-приложения
</a>
.
</>
@@ -97,7 +97,7 @@ export function InstanceGitlabConfigForm(props: Props) {
label: "Secret",
description: (
<>
The client secret is also found in your{" "}
Секрет клиента находится в настройках{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
@@ -105,7 +105,7 @@ export function InstanceGitlabConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
GitLab OAuth application settings
GitLab OAuth-приложения
</a>
.
</>
@@ -128,7 +128,7 @@ export function InstanceGitlabConfigForm(props: Props) {
url: `${originURL}/auth/gitlab/callback/`,
description: (
<>
We will auto-generate this. Paste this into the <CodeBlock darkerShade>Redirect URI</CodeBlock> field of your{" "}
Значение сформировано автоматически. Вставьте его в поле <CodeBlock darkerShade>Redirect URI</CodeBlock>{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
@@ -136,7 +136,7 @@ export function InstanceGitlabConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
GitLab OAuth application
GitLab OAuth-приложения
</a>
.
</>
@@ -151,8 +151,8 @@ export function InstanceGitlabConfigForm(props: Props) {
const response = await updateInstanceConfigurations(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your GitLab authentication is configured. You should test it now.",
title: "Готово",
message: "Аутентификация через GitLab настроена. Проверьте вход перед включением в проде.",
});
reset({
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
@@ -182,7 +182,7 @@ export function InstanceGitlabConfigForm(props: Props) {
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="pt-2.5 text-18 font-medium">GitLab-provided details for Plane</div>
<div className="pt-2.5 text-18 font-medium">Данные GitLab для NODE.DC</div>
{GITLAB_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
@@ -206,17 +206,17 @@ export function InstanceGitlabConfigForm(props: Props) {
loading={isSubmitting}
disabled={!isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
</Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
Go back
Назад
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-3 px-6 pt-1.5 pb-4">
<div className="pt-2 text-18 font-medium">Plane-provided details for GitLab</div>
<div className="nodedc-settings-helper-card flex flex-col gap-y-4 px-6 pt-1.5 pb-4">
<div className="pt-2 text-18 font-medium">Данные NODE.DC для GitLab</div>
{GITLAB_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -43,14 +43,14 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration",
loading: "Сохранение конфигурации",
success: {
title: "Configuration saved",
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
title: "Конфигурация сохранена",
message: () => `Вход через GitLab ${value === "1" ? "включен" : "отключен"}.`,
},
error: {
title: "Error",
message: () => "Failed to save configuration",
title: "Ошибка",
message: () => "Не удалось сохранить конфигурацию",
},
});
@@ -68,7 +68,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
customHeader={
<AuthenticationMethodCard
name="GitLab"
description="Allow members to login or sign up to plane with their GitLab accounts."
description="Вход и регистрация пользователей через аккаунты GitLab."
icon={<img src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
config={
<ToggleSwitch
@@ -104,6 +104,6 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
);
});
export const meta: Route.MetaFunction = () => [{ title: "GitLab Authentication - God Mode" }];
export const meta: Route.MetaFunction = () => [{ title: "GitLab OAuth - NODE.DC" }];
export default InstanceGitlabAuthenticationPage;
@@ -61,7 +61,7 @@ export function InstanceGoogleConfigForm(props: Props) {
label: "Client ID",
description: (
<>
Your client ID lives in your Google API Console.{" "}
Client ID находится в Google API Console.{" "}
<a
tabIndex={-1}
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
@@ -69,7 +69,7 @@ export function InstanceGoogleConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
Learn more
Подробнее
</a>
</>
),
@@ -83,7 +83,7 @@ export function InstanceGoogleConfigForm(props: Props) {
label: "Client secret",
description: (
<>
Your client secret should also be in your Google API Console.{" "}
Client secret также находится в Google API Console.{" "}
<a
tabIndex={-1}
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
@@ -91,7 +91,7 @@ export function InstanceGoogleConfigForm(props: Props) {
className="text-accent-primary hover:underline"
rel="noreferrer"
>
Learn more
Подробнее
</a>
</>
),
@@ -113,15 +113,15 @@ export function InstanceGoogleConfigForm(props: Props) {
url: originURL,
description: (
<p>
We will auto-generate this. Paste this into your{" "}
<CodeBlock darkerShade>Authorized JavaScript origins</CodeBlock> field. For this OAuth client{" "}
Значение сформировано автоматически. Вставьте его в поле{" "}
<CodeBlock darkerShade>Authorized JavaScript origins</CodeBlock> для OAuth-клиента{" "}
<a
href="https://console.cloud.google.com/apis/credentials/oauthclient"
target="_blank"
className="text-accent-primary hover:underline"
rel="noreferrer"
>
here.
здесь.
</a>
</p>
),
@@ -135,15 +135,15 @@ export function InstanceGoogleConfigForm(props: Props) {
url: `${originURL}/auth/google/callback/`,
description: (
<p>
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Redirect URI</CodeBlock>{" "}
field. For this OAuth client{" "}
Значение сформировано автоматически. Вставьте его в поле{" "}
<CodeBlock darkerShade>Authorized Redirect URI</CodeBlock> для OAuth-клиента{" "}
<a
href="https://console.cloud.google.com/apis/credentials/oauthclient"
target="_blank"
className="text-accent-primary hover:underline"
rel="noreferrer"
>
here.
здесь.
</a>
</p>
),
@@ -157,8 +157,8 @@ export function InstanceGoogleConfigForm(props: Props) {
const response = await updateInstanceConfigurations(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your Google authentication is configured. You should test it now.",
title: "Готово",
message: "Аутентификация через Google настроена. Проверьте вход перед включением в проде.",
});
reset({
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
@@ -187,7 +187,7 @@ export function InstanceGoogleConfigForm(props: Props) {
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="pt-2.5 text-18 font-medium">Google-provided details for Plane</div>
<div className="pt-2.5 text-18 font-medium">Данные Google для NODE.DC</div>
{GOOGLE_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
@@ -211,32 +211,32 @@ export function InstanceGoogleConfigForm(props: Props) {
loading={isSubmitting}
disabled={!isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
</Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
Go back
Назад
</Link>
</div>
</div>
</div>
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
<div className="pt-2 text-18 font-medium">Plane-provided details for Google</div>
<div className="pt-2 text-18 font-medium">Данные NODE.DC для Google</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
<div className="nodedc-settings-helper-card flex flex-col gap-y-4 px-6 py-4">
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
{/* web service details */}
<div className="flex flex-col overflow-hidden rounded-lg">
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
<div className="nodedc-settings-helper-card flex flex-col overflow-hidden">
<div className="nodedc-settings-helper-card-header flex items-center gap-x-3 px-6 py-3 text-11 font-medium uppercase">
<Monitor className="h-3 w-3" />
Web
Веб
</div>
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
<div className="flex flex-col gap-y-4 px-6 py-4">
{GOOGLE_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -43,14 +43,14 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration",
loading: "Сохранение конфигурации",
success: {
title: "Configuration saved",
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
title: "Конфигурация сохранена",
message: () => `Вход через Google ${value === "1" ? "включен" : "отключен"}.`,
},
error: {
title: "Error",
message: () => "Failed to save configuration",
title: "Ошибка",
message: () => "Не удалось сохранить конфигурацию",
},
});
@@ -68,8 +68,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
customHeader={
<AuthenticationMethodCard
name="Google"
description="Allow members to login or sign up to plane with their Google
accounts."
description="Вход и регистрация пользователей через аккаунты Google."
icon={<img src={GoogleLogo} height={24} width={24} alt="Google Logo" />}
config={
<ToggleSwitch
@@ -105,6 +104,6 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
);
});
export const meta: Route.MetaFunction = () => [{ title: "Google Authentication - God Mode" }];
export const meta: Route.MetaFunction = () => [{ title: "Google OAuth - NODE.DC" }];
export default InstanceGoogleAuthenticationPage;
@@ -55,9 +55,8 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
if (!canDisable) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Cannot disable authentication",
message:
"At least one authentication method must remain enabled. Please enable another method before disabling this one.",
title: "Нельзя отключить вход",
message: "Должен остаться хотя бы один способ входа. Сначала включите другой способ аутентификации.",
});
return;
}
@@ -74,14 +73,14 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving configuration",
loading: "Сохранение конфигурации",
success: {
title: "Success",
message: () => "Configuration saved successfully",
title: "Сохранено",
message: () => "Конфигурация обновлена",
},
error: {
title: "Error",
message: () => "Failed to save configuration",
title: "Ошибка",
message: () => "Не удалось сохранить конфигурацию",
},
});
@@ -111,8 +110,8 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
return (
<PageWrapper
header={{
title: "Manage authentication modes for your instance",
description: "Configure authentication modes for your team and restrict sign-ups to be invite only.",
title: "Способы входа в инстанс",
description: "Настройте email, пароль, OAuth-провайдеры и правила регистрации пользователей.",
}}
>
{formattedConfig ? (
@@ -120,9 +119,9 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="pb-1 text-16 font-medium">Allow anyone to sign up even without an invite</div>
<div className="pb-1 text-16 font-medium">Разрешить регистрацию без приглашения</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
Toggling this off will only let users sign up when they are invited.
Если выключить, новые пользователи смогут зарегистрироваться только по приглашению.
</div>
</div>
</div>
@@ -143,7 +142,7 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
</div>
</div>
</div>
<div className="text-lg pt-6 font-medium">Available authentication modes</div>
<div className="text-lg pt-6 font-medium">Доступные способы входа</div>
{authenticationModes.map((method) => (
<AuthenticationMethodCard
key={method.key}
@@ -169,6 +168,6 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
);
});
export const meta: Route.MetaFunction = () => [{ title: "Authentication Settings - Plane Web" }];
export const meta: Route.MetaFunction = () => [{ title: "Аутентификация - NODE.DC" }];
export default InstanceAuthenticationPage;
@@ -31,7 +31,7 @@ type TEmailSecurityKeys = "EMAIL_USE_TLS" | "EMAIL_USE_SSL" | "NONE";
const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
EMAIL_USE_TLS: "TLS",
EMAIL_USE_SSL: "SSL",
NONE: "No email security",
NONE: "Без шифрования",
};
export function InstanceEmailForm(props: IInstanceEmailForm) {
@@ -63,7 +63,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
{
key: "EMAIL_HOST",
type: "text",
label: "Host",
label: "Хост",
placeholder: "email.google.com",
error: Boolean(errors.EMAIL_HOST),
required: true,
@@ -71,7 +71,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
{
key: "EMAIL_PORT",
type: "text",
label: "Port",
label: "Порт",
placeholder: "8080",
error: Boolean(errors.EMAIL_PORT),
required: true,
@@ -79,9 +79,9 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
{
key: "EMAIL_FROM",
type: "text",
label: "Sender's email address",
label: "Email отправителя",
description:
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
"Этот адрес будут видеть пользователи в письмах от инстанса. Адрес нужно подтвердить на стороне SMTP.",
placeholder: "no-reply@projectplane.so",
error: Boolean(errors.EMAIL_FROM),
required: true,
@@ -92,7 +92,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
{
key: "EMAIL_HOST_USER",
type: "text",
label: "Username",
label: "Имя пользователя",
placeholder: "getitdone@projectplane.so",
error: Boolean(errors.EMAIL_HOST_USER),
required: false,
@@ -100,8 +100,8 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
{
key: "EMAIL_HOST_PASSWORD",
type: "password",
label: "Password",
placeholder: "Password",
label: "Пароль",
placeholder: "Пароль",
error: Boolean(errors.EMAIL_HOST_PASSWORD),
required: false,
},
@@ -114,8 +114,8 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
.then(() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success",
message: "Email Settings updated successfully",
title: "Сохранено",
message: "Настройки почты обновлены",
})
)
.catch((err) => console.error(err));
@@ -163,12 +163,12 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
/>
))}
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Email security</h4>
<h4 className="text-13 text-tertiary">Защита соединения</h4>
<CustomSelect
value={emailSecurityKey}
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
onChange={handleEmailSecurityChange}
buttonClassName="rounded-md border-subtle"
buttonClassName="nodedc-settings-select rounded-md border-subtle"
input
>
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
@@ -183,9 +183,9 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
<div className="mr-8 flex items-center gap-10 pt-4">
<div className="grow">
<div className="text-13 font-medium text-primary">Authentication</div>
<div className="text-13 font-medium text-primary">Аутентификация</div>
<div className="text-11 font-regular text-tertiary">
This is optional, but we recommend setting up a username and a password for your SMTP server.
Необязательно, но для SMTP-сервера обычно нужны имя пользователя и пароль.
</div>
</div>
</div>
@@ -215,7 +215,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
loading={isSubmitting}
disabled={!isValid || !isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
</Button>
<Button
variant="secondary"
@@ -224,7 +224,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
loading={isSubmitting}
disabled={!isValid}
>
Send test email
Отправить тестовое письмо
</Button>
</div>
</div>
@@ -34,14 +34,14 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
await disableEmail();
setIsSMTPEnabled(false);
setToast({
title: "Email feature disabled",
message: "Email feature has been disabled",
title: "Почта отключена",
message: "Отправка писем через SMTP отключена",
type: TOAST_TYPE.SUCCESS,
});
} catch (_error) {
setToast({
title: "Error disabling email",
message: "Failed to disable email feature. Please try again.",
title: "Не удалось отключить почту",
message: "Повторите попытку.",
type: TOAST_TYPE.ERROR,
});
} finally {
@@ -60,13 +60,13 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
return (
<PageWrapper
header={{
title: "Secure emails from your own instance",
title: "Письма от вашего инстанса",
description: (
<>
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
NODE.DC может отправлять системные письма пользователям через ваш SMTP-сервер.
<div className="text-13 font-regular text-tertiary">
Set it up below and please test your settings before you save them.&nbsp;
<span className="text-danger-primary">Misconfigs can lead to email bounces and errors.</span>
Заполните параметры ниже и проверьте отправку перед сохранением.&nbsp;
<span className="text-danger-primary">Ошибки в конфигурации приводят к отказам доставки.</span>
</div>
</>
),
@@ -98,6 +98,6 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
);
});
export const meta: Route.MetaFunction = () => [{ title: "Email Settings - God Mode" }];
export const meta: Route.MetaFunction = () => [{ title: "Настройки почты - NODE.DC" }];
export default InstanceEmailPage;
@@ -58,7 +58,7 @@ export function SendTestEmailModal(props: Props) {
setSendEmailStep(ESendEmailSteps.SUCCESS);
})
.catch((error) => {
setError(error?.error || "Failed to send email");
setError(error?.error || "Не удалось отправить письмо");
setSendEmailStep(ESendEmailSteps.FAILED);
})
.finally(() => {
@@ -91,13 +91,13 @@ export function SendTestEmailModal(props: Props) {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative w-full transform rounded-lg bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all sm:max-w-xl">
<Dialog.Panel className="nodedc-glass-modal relative w-full transform rounded-[1.75rem] bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all sm:max-w-xl">
<h3 className="text-16 leading-6 font-medium text-primary">
{sendEmailStep === ESendEmailSteps.SEND_EMAIL
? "Send test email"
? "Отправить тестовое письмо"
: sendEmailStep === ESendEmailSteps.SUCCESS
? "Email send"
: "Failed"}{" "}
? "Письмо отправлено"
: "Ошибка отправки"}{" "}
</h3>
<div className="pt-6 pb-2">
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
@@ -106,28 +106,25 @@ export function SendTestEmailModal(props: Props) {
type="email"
value={receiverEmail}
onChange={(e) => setReceiverEmail(e.target.value)}
placeholder="Receiver email"
className="w-full resize-none text-16"
placeholder="Email получателя"
className="nodedc-settings-input w-full resize-none text-16"
tabIndex={1}
/>
)}
{sendEmailStep === ESendEmailSteps.SUCCESS && (
<div className="flex flex-col gap-y-4 text-13">
<p>
We have sent the test email to {receiverEmail}. Please check your spam folder if you cannot find
it.
</p>
<p>If you still cannot find it, recheck your SMTP configuration and trigger a new test email.</p>
<p>Тестовое письмо отправлено на {receiverEmail}. Если письма нет во входящих, проверьте спам.</p>
<p>Если письмо не пришло, проверьте SMTP-настройки и отправьте тест заново.</p>
</div>
)}
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
<div className="mt-5 flex items-center justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Отмена" : "Закрыть"}
</Button>
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
{isLoading ? "Sending email" : "Send email"}
{isLoading ? "Отправка" : "Отправить"}
</Button>
)}
</div>
@@ -60,8 +60,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
.then(() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success",
message: "Settings updated successfully",
title: "Сохранено",
message: "Основные настройки обновлены",
})
)
.catch((err) => console.error(err));
@@ -70,41 +70,41 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
return (
<div className="space-y-8">
<div className="space-y-4">
<div className="text-16 font-medium text-primary">Instance details</div>
<div className="text-16 font-medium text-primary">Данные инстанса</div>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-8 md:grid-cols-2 lg:grid-cols-3">
<ControllerInput
key="instance_name"
name="instance_name"
control={control}
type="text"
label="Name of instance"
placeholder="Instance name"
label="Название инстанса"
placeholder="Название инстанса"
error={Boolean(errors.instance_name)}
required
/>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Email</h4>
<h4 className="text-13 text-tertiary">Email администратора</h4>
<Input
id="email"
name="email"
type="email"
value={instanceAdmins[0]?.user_detail?.email ?? ""}
placeholder="Admin email"
className="w-full cursor-not-allowed !text-placeholder"
placeholder="Email администратора"
className="nodedc-settings-input w-full cursor-not-allowed !text-placeholder"
autoComplete="on"
disabled
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Instance ID</h4>
<h4 className="text-13 text-tertiary">ID инстанса</h4>
<Input
id="instance_id"
name="instance_id"
type="text"
value={instance.instance_id}
className="w-full cursor-not-allowed rounded-md font-medium !text-placeholder"
className="nodedc-settings-input w-full cursor-not-allowed rounded-md font-medium !text-placeholder"
disabled
/>
</div>
@@ -112,7 +112,7 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
</div>
<div className="space-y-6">
<div className="border-b border-subtle pb-1.5 text-16 font-medium text-primary">Chat + telemetry</div>
<div className="border-b border-subtle pb-1.5 text-16 font-medium text-primary">Чат и телеметрия</div>
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
<div className="flex items-center gap-14">
<div className="flex grow items-center gap-4">
@@ -122,17 +122,17 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
</div>
</div>
<div className="grow">
<div className="text-13 leading-5 font-medium text-primary">Let Plane collect anonymous usage data</div>
<div className="text-13 leading-5 font-medium text-primary">Разрешить анонимную телеметрию</div>
<div className="text-11 leading-5 font-regular text-tertiary">
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
in line with{" "}
Персональные данные не собираются. Анонимные события помогают понимать, как используется система, с
учетом{" "}
<a
href="https://developers.plane.so/self-hosting/telemetry"
target="_blank"
className="text-accent-primary hover:underline"
rel="noreferrer"
>
our Telemetry Policy.
политики телеметрии.
</a>
</div>
</div>
@@ -158,7 +158,7 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
}}
loading={isSubmitting}
>
{isSubmitting ? "Saving" : "Save changes"}
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
</Button>
</div>
</div>
@@ -64,10 +64,10 @@ export const IntercomConfig = observer(function IntercomConfig(props: TIntercomC
</div>
<div className="grow">
<div className="text-13 leading-5 font-medium text-primary">Chat with us</div>
<div className="text-13 leading-5 font-medium text-primary">Встроенный чат поддержки</div>
<div className="text-11 leading-5 font-regular text-tertiary">
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
automatically.
Разрешает пользователям писать в поддержку через Intercom или аналогичный сервис. При отключении
телеметрии чат отключается автоматически.
</div>
</div>
@@ -20,9 +20,8 @@ function GeneralPage() {
return (
<PageWrapper
header={{
title: "General settings",
description:
"Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your instance.",
title: "Основные настройки",
description: "Имя инстанса, служебные идентификаторы, email администратора и режимы телеметрии.",
}}
>
{instance && instanceAdmins && <GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />}
@@ -30,6 +29,6 @@ function GeneralPage() {
);
}
export const meta: Route.MetaFunction = () => [{ title: "General Settings - God Mode" }];
export const meta: Route.MetaFunction = () => [{ title: "Основные настройки - NODE.DC" }];
export default observer(GeneralPage);
@@ -41,8 +41,8 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
.then(() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success",
message: "Image Configuration Settings updated successfully",
title: "Сохранено",
message: "Настройки изображений обновлены",
})
)
.catch((err) => console.error(err));
@@ -55,17 +55,17 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
control={control}
type="password"
name="UNSPLASH_ACCESS_KEY"
label="Access key from your Unsplash account"
label="Access key аккаунта Unsplash"
description={
<>
You will find your access key in your Unsplash developer console.&nbsp;
Ключ доступа находится в консоли разработчика Unsplash.&nbsp;
<a
href="https://unsplash.com/documentation#creating-a-developer-account"
target="_blank"
className="text-accent-primary hover:underline"
rel="noreferrer"
>
Learn more.
Подробнее.
</a>
</>
}
@@ -77,7 +77,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
<div>
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving" : "Save changes"}
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
</Button>
</div>
</div>
@@ -25,8 +25,8 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
return (
<PageWrapper
header={{
title: "Third-party image libraries",
description: "Let your users search and choose images from third-party libraries",
title: "Внешние библиотеки изображений",
description: "Разрешите пользователям искать и выбирать изображения из внешних библиотек.",
}}
>
{formattedConfig ? (
@@ -41,6 +41,6 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
);
});
export const meta: Route.MetaFunction = () => [{ title: "Images Settings - God Mode" }];
export const meta: Route.MetaFunction = () => [{ title: "Изображения - NODE.DC" }];
export default InstanceImagePage;
@@ -38,9 +38,9 @@ function AdminLayout(_props: Route.ComponentProps) {
if (isUserLoggedIn) {
return (
<div className="relative flex h-screen w-screen overflow-hidden">
<div className="nodedc-admin-shell relative flex h-screen w-screen overflow-hidden">
<AdminSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
<main className="nodedc-admin-main relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
<AdminHeader />
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-scroll">
<Outlet />
@@ -36,11 +36,13 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
const handleSignOut = () => signOut();
const getSidebarMenuItems = () => (
const getSidebarMenuItems = (align: "left" | "right" = "left") => (
<Menu.Items
className={cn(
"shadow-lg absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-subtle rounded-md border border-subtle bg-surface-1 px-1 py-2 text-11 outline-none",
"nodedc-glass-popup-surface absolute z-20 mt-1.5 flex w-56 flex-col divide-y divide-white/6 px-2 py-2 text-11 outline-none",
{
"left-0": align === "left",
"right-0": align === "right",
"left-4": isSidebarCollapsed,
}
)}
@@ -52,11 +54,11 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
className="nodedc-settings-sidebar-item flex w-full items-center gap-2 px-2 py-1 text-left"
onClick={handleThemeSwitch}
>
<Palette className="h-4 w-4 stroke-[1.5]" />
Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode
{resolvedTheme === "dark" ? "Светлая тема" : "Темная тема"}
</Menu.Item>
</div>
<div className="py-2">
@@ -65,10 +67,10 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
<Menu.Item
as="button"
type="submit"
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
className="nodedc-settings-sidebar-item flex w-full items-center gap-2 px-2 py-1 text-left"
>
<LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out
Выйти
</Menu.Item>
</form>
</div>
@@ -81,10 +83,10 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
}, [csrfToken]);
return (
<div className="flex max-h-header items-center gap-x-5 gap-y-2 border-b border-subtle px-4 py-2.5">
<div className="px-3 pt-4 pb-2">
<div className="h-full w-full truncate">
<div
className={`flex flex-grow items-center gap-x-2 truncate rounded-sm ${
className={`nodedc-admin-sidebar-profile flex flex-grow items-center gap-x-3 truncate px-3 py-3 ${
isSidebarCollapsed ? "justify-center" : ""
}`}
>
@@ -94,8 +96,8 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
"cursor-default": !isSidebarCollapsed,
})}
>
<div className="flex size-8 flex-shrink-0 items-center justify-center rounded-sm bg-layer-1">
<UserCog2 className="size-5 text-primary" />
<div className="nodedc-admin-sidebar-avatar-button flex size-10 flex-shrink-0 items-center justify-center">
<UserCog2 className="size-5 text-[rgb(var(--nodedc-card-active-rgb))]" />
</div>
</Menu.Button>
{isSidebarCollapsed && (
@@ -114,38 +116,39 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
</Menu>
{!isSidebarCollapsed && (
<div className="flex w-full gap-2">
<h4 className="grow truncate text-body-md-medium text-primary">Instance admin</h4>
<div className="min-w-0 flex-1">
<h4 className="truncate text-15 font-medium text-primary">Глобальный админ</h4>
<div className="truncate text-11 font-medium text-tertiary">Супер-администратор</div>
</div>
)}
{!isSidebarCollapsed && currentUser && (
<Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="nodedc-admin-sidebar-action grid size-8 place-items-center outline-none">
<Avatar
name={currentUser.display_name}
src={getFileURL(currentUser.avatar_url)}
size={24}
shape="square"
className="!text-body-sm-medium"
/>
</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"
>
{getSidebarMenuItems("right")}
</Transition>
</Menu>
)}
</div>
</div>
{!isSidebarCollapsed && currentUser && (
<Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="grid place-items-center outline-none">
<Avatar
name={currentUser.display_name}
src={getFileURL(currentUser.avatar_url)}
size={24}
shape="square"
className="!text-body-sm-medium"
/>
</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"
>
{getSidebarMenuItems()}
</Transition>
</Menu>
)}
</div>
);
});
@@ -20,17 +20,17 @@ import { useInstance, useTheme } from "@/hooks/store";
const helpOptions = [
{
name: "Documentation",
name: "Документация",
href: "https://docs.plane.so/",
Icon: PageIcon,
},
{
name: "Join our Forum",
name: "Форум Plane",
href: "https://forum.plane.so",
Icon: MessageSquare,
},
{
name: "Report a bug",
name: "Сообщить об ошибке",
href: "https://github.com/makeplane/plane/issues/new/choose",
Icon: GithubIcon,
},
@@ -50,26 +50,26 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
return (
<div
className={cn(
"flex h-14 w-full flex-shrink-0 items-center justify-between gap-1 self-baseline border-t border-subtle bg-surface-1 px-4",
"flex h-16 w-full flex-shrink-0 items-center justify-between gap-1 self-baseline border-t border-white/6 px-3",
{
"h-auto flex-col py-1.5": isSidebarCollapsed,
}
)}
>
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<Tooltip tooltipContent="Перейти в приложение" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<a
href={redirectionLink}
className={`relative flex items-center gap-1 rounded-sm bg-layer-1 px-2 py-1 text-body-xs-medium whitespace-nowrap text-secondary`}
className="nodedc-admin-sidebar-action relative flex items-center gap-1 px-3 py-1 text-12 font-medium whitespace-nowrap"
>
<NewTabIcon width={14} height={14} />
{!isSidebarCollapsed && "Redirect to Plane"}
{!isSidebarCollapsed && "В приложение"}
</a>
</Tooltip>
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<Tooltip tooltipContent="Помощь" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
className={`ml-auto grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
className={`nodedc-admin-sidebar-action ml-auto grid place-items-center p-1.5 outline-none ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
@@ -77,10 +77,10 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
<HelpCircle className="size-4" />
</button>
</Tooltip>
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<Tooltip tooltipContent="Свернуть меню" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
className={`grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
className={`nodedc-admin-sidebar-action grid place-items-center p-1.5 outline-none ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => toggleSidebar(!isSidebarCollapsed)}
@@ -103,7 +103,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
<div
className={`absolute bottom-2 z-[15] min-w-[10rem] ${
isSidebarCollapsed ? "left-full" : "-left-[75px]"
} divide-y divide-subtle-1 rounded-sm bg-surface-1 p-1 whitespace-nowrap shadow-raised-100`}
} nodedc-glass-popup-surface divide-y divide-subtle-1 rounded-sm bg-surface-1 p-1 whitespace-nowrap shadow-raised-100`}
ref={helpOptionsRef}
>
<div className="space-y-1 pb-2">
@@ -134,7 +134,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
);
})}
</div>
<div className="px-2 pt-2 pb-1 text-10">Version: v{instance?.current_version}</div>
<div className="px-2 pt-2 pb-1 text-10">Версия: v{instance?.current_version}</div>
</div>
</Transition>
</div>
@@ -21,6 +21,10 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
const { isSidebarCollapsed, toggleSidebar } = useTheme();
// derived values
const sidebarMenu = useSidebarMenu();
const sidebarMenuGroups = [
{ label: "ИНСТАНС", items: sidebarMenu.slice(0, 4) },
{ label: "ВОЗМОЖНОСТИ", items: sidebarMenu.slice(4) },
];
const handleItemClick = () => {
if (window.innerWidth < 768) {
@@ -29,36 +33,33 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
};
return (
<div className="vertical-scrollbar flex scrollbar-sm h-full w-full flex-col gap-2.5 overflow-y-scroll px-4 py-4">
{sidebarMenu.map((item, index) => {
const isActive = item.href === pathName || pathName?.includes(item.href);
return (
<Link key={index} href={item.href} onClick={handleItemClick}>
<div>
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
<div
className={cn(
"group flex w-full items-center gap-3 rounded-md px-3 py-2 transition-colors outline-none",
{
"!bg-layer-transparent-active text-primary": isActive,
"text-secondary hover:bg-layer-transparent-hover active:bg-layer-transparent-active": !isActive,
},
isSidebarCollapsed ? "justify-center" : "w-[260px]"
)}
>
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
{!isSidebarCollapsed && (
<div className="w-full">
<div className={cn(`text-body-xs-medium transition-colors`)}>{item.name}</div>
<div className={cn(`text-caption-sm-regular transition-colors`)}>{item.description}</div>
<div className="vertical-scrollbar flex scrollbar-sm h-full w-full flex-col overflow-y-scroll px-3 py-4">
{sidebarMenuGroups.map((group) => (
<div key={group.label} className="shrink-0 border-b border-white/6 py-3 first:pt-0 last:border-b-0 last:pb-0">
{!isSidebarCollapsed && <div className="nodedc-admin-sidebar-section-label">{group.label}</div>}
<div className="flex flex-col gap-1">
{group.items.map((item) => {
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
return (
<Link key={item.href} href={item.href} onClick={handleItemClick}>
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
<div
className={cn(
"nodedc-settings-sidebar-item group flex w-full items-center gap-3 px-3 py-2 text-14 font-medium transition-colors outline-none",
isSidebarCollapsed ? "justify-center" : "w-full"
)}
data-active={isActive}
>
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
{!isSidebarCollapsed && <div className="min-w-0 truncate transition-colors">{item.name}</div>}
</div>
)}
</div>
</Tooltip>
</div>
</Link>
);
})}
</Tooltip>
</Link>
);
})}
</div>
</div>
))}
</div>
);
});
@@ -44,7 +44,7 @@ export const AdminSidebar = observer(function AdminSidebar() {
return (
<div
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-subtle bg-surface-1 duration-300 md:relative ${isSidebarCollapsed ? "-ml-[290px]" : ""} sm:${isSidebarCollapsed ? "-ml-[290px]" : ""} md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} `}
className={`nodedc-glass-sidebar fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-subtle bg-surface-1 duration-300 md:relative ${isSidebarCollapsed ? "-ml-[290px]" : ""} sm:${isSidebarCollapsed ? "-ml-[290px]" : ""} md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} `}
>
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
<AdminSidebarDropdown />
@@ -56,16 +56,16 @@ export function WorkspaceCreateForm() {
.then(async () => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Workspace created successfully.",
title: "Готово",
message: "Воркспейс создан.",
});
router.push(`/workspace`);
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Workspace could not be created. Please try again.",
title: "Ошибка",
message: "Не удалось создать воркспейс. Попробуйте еще раз.",
});
});
} else setSlugError(true);
@@ -73,8 +73,8 @@ export function WorkspaceCreateForm() {
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Some error occurred while creating workspace. Please try again.",
title: "Ошибка",
message: "При создании воркспейса произошла ошибка. Попробуйте еще раз.",
});
});
};
@@ -91,7 +91,7 @@ export function WorkspaceCreateForm() {
<div className="space-y-8">
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Name your workspace</h4>
<h4 className="text-13 text-tertiary">Название воркспейса</h4>
<div className="flex flex-col gap-1">
<Controller
control={control}
@@ -113,8 +113,8 @@ export function WorkspaceCreateForm() {
}}
ref={ref}
hasError={Boolean(errors.name)}
placeholder="Something familiar and recognizable is always best."
className="w-full"
placeholder="Короткое понятное название"
className="nodedc-settings-input w-full"
/>
)}
/>
@@ -122,8 +122,8 @@ export function WorkspaceCreateForm() {
</div>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Set your workspace&apos;s URL</h4>
<div className="flex w-full items-center gap-0.5 rounded-md border-[0.5px] border-subtle px-3">
<h4 className="text-13 text-tertiary">URL воркспейса</h4>
<div className="nodedc-settings-input flex w-full items-center gap-0.5 rounded-md border-[0.5px] border-subtle px-3">
<span className="text-13 whitespace-nowrap text-secondary">{workspaceBaseURL}</span>
<Controller
control={control}
@@ -149,29 +149,29 @@ export function WorkspaceCreateForm() {
)}
/>
</div>
{slugError && <p className="text-13 text-danger-primary">This URL is taken. Try something else.</p>}
{slugError && <p className="text-13 text-danger-primary">Этот URL уже занят. Выберите другой.</p>}
{invalidSlug && (
<p className="text-13 text-danger-primary">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
<p className="text-13 text-danger-primary">{`URL может содержать только латинские буквы, цифры, "-" и "_".`}</p>
)}
{errors.slug && <span className="text-11 text-danger-primary">{errors.slug.message}</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">How many people will use this workspace?</h4>
<h4 className="text-13 text-tertiary">Сколько людей будет работать в воркспейсе?</h4>
<div className="w-full">
<Controller
name="organization_size"
control={control}
rules={{ required: "This is a required field." }}
rules={{ required: "Это обязательное поле." }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? (
<span className="text-placeholder">Select a range</span>
<span className="text-placeholder">Выберите диапазон</span>
)
}
buttonClassName="!border-[0.5px] !border-subtle !shadow-none"
buttonClassName="nodedc-settings-select !border-[0.5px] !border-subtle !shadow-none"
input
>
{ORGANIZATION_SIZE.map((item) => (
@@ -196,10 +196,10 @@ export function WorkspaceCreateForm() {
disabled={!isValid}
loading={isSubmitting}
>
{isSubmitting ? "Creating workspace" : "Create workspace"}
{isSubmitting ? "Создание" : "Создать воркспейс"}
</Button>
<Link className={getButtonStyling("secondary", "lg")} href="/workspace">
Go back
Назад
</Link>
</div>
</div>
@@ -16,8 +16,8 @@ const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.
return (
<PageWrapper
header={{
title: "Create a new workspace on this instance.",
description: "You will need to invite users from Workspace Settings after you create this workspace.",
title: "Создать новый воркспейс",
description: "После создания пригласите пользователей в настройках рабочего пространства.",
}}
>
<WorkspaceCreateForm />
@@ -25,6 +25,6 @@ const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.
);
});
export const meta: Route.MetaFunction = () => [{ title: "Create Workspace - God Mode" }];
export const meta: Route.MetaFunction = () => [{ title: "Создание воркспейса - NODE.DC" }];
export default WorkspaceCreatePage;
@@ -5,14 +5,16 @@
*/
import Link from "next/link";
import { PlaneLockup } from "@plane/propel/icons";
export function AuthHeader() {
return (
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-primary" />
<span className="tracking-normal text-16 font-semibold text-primary">NODE.DC</span>
</Link>
<span className="rounded-full bg-white/6 px-3 py-1 text-11 font-medium text-secondary">
Глобальное администрирование
</span>
</div>
);
}
@@ -22,56 +22,56 @@ const errorCodeMessages: {
} = {
// admin
[EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST]: {
title: `Admin already exists`,
message: () => `Admin already exists. Please try again.`,
title: `Администратор уже существует`,
message: () => `Администратор уже существует. Попробуйте еще раз.`,
},
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
title: `Email, password and first name required`,
message: () => `Email, password and first name required. Please try again.`,
title: `Нужны email, пароль и имя`,
message: () => `Укажите email, пароль и имя. Попробуйте еще раз.`,
},
[EAdminAuthErrorCodes.INVALID_ADMIN_EMAIL]: {
title: `Invalid admin email`,
message: () => `Invalid admin email. Please try again.`,
title: `Некорректный email администратора`,
message: () => `Некорректный email администратора. Попробуйте еще раз.`,
},
[EAdminAuthErrorCodes.INVALID_ADMIN_PASSWORD]: {
title: `Invalid admin password`,
message: () => `Invalid admin password. Please try again.`,
title: `Некорректный пароль администратора`,
message: () => `Некорректный пароль администратора. Попробуйте еще раз.`,
},
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
title: `Нужны email и пароль`,
message: () => `Укажите email и пароль. Попробуйте еще раз.`,
},
[EAdminAuthErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
title: `Ошибка входа`,
message: () => `Не удалось войти. Проверьте данные и попробуйте еще раз.`,
},
[EAdminAuthErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
title: `Admin user already exists`,
title: `Администратор уже существует`,
message: () => (
<div>
Admin user already exists.&nbsp;
Администратор уже существует.&nbsp;
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
Sign In
Войти
</Link>
&nbsp;now.
&nbsp;сейчас.
</div>
),
},
[EAdminAuthErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
title: `Admin user does not exist`,
title: `Администратор не найден`,
message: () => (
<div>
Admin user does not exist.&nbsp;
Администратор не найден.&nbsp;
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
Sign In
Войти
</Link>
&nbsp;now.
&nbsp;сейчас.
</div>
),
},
[EAdminAuthErrorCodes.ADMIN_USER_DEACTIVATED]: {
title: `User account deactivated`,
message: () => `User account deactivated. Please contact ${SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
title: `Аккаунт деактивирован`,
message: () => `Аккаунт деактивирован. Свяжитесь с ${SUPPORT_EMAIL ? SUPPORT_EMAIL : "администратором"}.`,
},
};
@@ -92,8 +92,8 @@ export const authErrorHandler = (errorCode: EAdminAuthErrorCodes, email?: string
return {
type: EErrorAlertType.BANNER_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
title: errorCodeMessages[errorCode]?.title || "Ошибка",
message: errorCodeMessages[errorCode]?.message(email) || "Что-то пошло не так. Попробуйте еще раз.",
};
return undefined;
@@ -22,7 +22,7 @@ function RootLayout() {
}, [replace, isUserLoggedIn]);
return (
<div className="relative z-10 flex h-screen w-screen flex-col items-center overflow-hidden overflow-y-auto bg-surface-1 px-8 pt-6 pb-10">
<div className="nodedc-auth-shell relative z-10 flex h-screen w-screen flex-col items-center overflow-hidden overflow-y-auto bg-surface-1 px-8 pt-6 pb-10">
<Outlet />
</div>
);
@@ -45,6 +45,6 @@ function HomePage() {
export default observer(HomePage);
export const meta: Route.MetaFunction = () => [
{ title: "Admin – Instance Setup & Sign-In" },
{ name: "description", content: "Configure your Plane instance or sign in to the admin portal." },
{ title: "NODE.DC - вход в админ-панель" },
{ name: "description", content: "Настройка инстанса NODE.DC и вход в глобальную админ-панель." },
];
@@ -112,10 +112,10 @@ export function InstanceSignInForm() {
<>
<AuthHeader />
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
<div className="nodedc-auth-card relative flex w-full max-w-[22.5rem] flex-col gap-6">
<FormHeader
heading="Manage your Plane instance"
subHeading="Configure instance-wide settings to secure your instance"
heading="Управление инстансом NODE.DC"
subHeading="Войдите, чтобы менять глобальные настройки системы"
/>
<form
className="space-y-4"
@@ -135,10 +135,10 @@ export function InstanceSignInForm() {
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="email">
Email <span className="text-danger-primary">*</span>
Электронная почта <span className="text-danger-primary">*</span>
</label>
<Input
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
id="email"
name="email"
type="email"
@@ -153,16 +153,16 @@ export function InstanceSignInForm() {
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="password">
Password <span className="text-danger-primary">*</span>
Пароль <span className="text-danger-primary">*</span>
</label>
<div className="relative">
<Input
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
id="password"
name="password"
type={showPassword ? "text" : "password"}
inputSize="md"
placeholder="Enter your password"
placeholder="Введите пароль"
value={formData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
autoComplete="off"
@@ -188,7 +188,7 @@ export function InstanceSignInForm() {
</div>
<div className="py-2">
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Войти"}
</Button>
</div>
</form>
+4 -7
View File
@@ -17,19 +17,16 @@ function PageNotFound() {
<div className="grid h-full place-items-center p-4">
<div className="space-y-8 text-center">
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
<img src={Image404} alt="404 - Page not found" className="h-full w-full object-contain" />
<img src={Image404} alt="404 - страница не найдена" className="h-full w-full object-contain" />
</div>
<div className="space-y-2">
<h3 className="text-16 font-semibold">Oops! Something went wrong.</h3>
<p className="text-13 text-secondary">
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
temporarily unavailable.
</p>
<h3 className="text-16 font-semibold">Страница не найдена</h3>
<p className="text-13 text-secondary">Похоже, раздел был удален, переименован или временно недоступен.</p>
</div>
<Link to="/general/">
<span className="flex justify-center py-4">
<Button variant="secondary" size="lg">
Go to general settings
Перейти в основные настройки
</Button>
</span>
</Link>
+7 -9
View File
@@ -21,9 +21,8 @@ import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wgh
import "@fontsource/material-symbols-rounded";
import "@fontsource/ibm-plex-mono";
const APP_TITLE = "Plane | Simple, extensible, open-source project management tool.";
const APP_DESCRIPTION =
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.";
const APP_TITLE = "NODE.DC | Глобальное администрирование";
const APP_DESCRIPTION = "Панель глобального администрирования инстанса NODE.DC.";
export const links: LinksFunction = () => [
{ rel: "apple-touch-icon", sizes: "180x180", href: appleTouchIcon },
@@ -43,7 +42,7 @@ export const links: LinksFunction = () => [
export function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<html lang="ru" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -66,15 +65,14 @@ export const meta: Route.MetaFunction = () => [
{ property: "og:url", content: "https://plane.so/" },
{
name: "keywords",
content:
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration",
content: "NODE.DC, администрирование, рабочие пространства, проекты, пользователи, настройки инстанса",
},
{ name: "twitter:site", content: "@planepowers" },
{ name: "twitter:site", content: "@nodedc" },
];
export default function Root() {
return (
<div className="min-h-screen bg-canvas">
<div className="nodedc-admin-root min-h-screen bg-canvas">
<Outlet />
</div>
);
@@ -91,7 +89,7 @@ export function HydrateFallback() {
export function ErrorBoundary({ error: _error }: Route.ErrorBoundaryProps) {
return (
<div>
<p>Something went wrong.</p>
<p>Что-то пошло не так.</p>
</div>
);
}