mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
E2C: Implement on-prem auth flow (#83513)
* add connection modal * extend api with connect/disconnect endpoints * extract translations * display stack url * use react-hook-form * fix links spanning whole modal * review comments
This commit is contained in:
parent
036e19037e
commit
1994d1e2c7
@ -29,16 +29,24 @@ function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryF
|
||||
|
||||
interface MigrateToCloudStatusDTO {
|
||||
enabled: boolean;
|
||||
stackURL?: string;
|
||||
}
|
||||
|
||||
interface CreateMigrationTokenResponseDTO {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface ConnectStackDTO {
|
||||
stackURL: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
// TODO remove these mock properties/functions
|
||||
const MOCK_DELAY_MS = 1000;
|
||||
const MOCK_TOKEN = 'TODO_thisWillBeABigLongToken';
|
||||
let HAS_MIGRATION_TOKEN = false;
|
||||
let HAS_STACK_DETAILS = false;
|
||||
let STACK_URL: string | undefined;
|
||||
|
||||
function dataWithMockDelay<T>(data: T): Promise<{ data: T }> {
|
||||
return new Promise((resolve) => {
|
||||
@ -49,13 +57,35 @@ function dataWithMockDelay<T>(data: T): Promise<{ data: T }> {
|
||||
}
|
||||
|
||||
export const migrateToCloudAPI = createApi({
|
||||
tagTypes: ['migrationToken'],
|
||||
tagTypes: ['migrationToken', 'stackDetails'],
|
||||
reducerPath: 'migrateToCloudAPI',
|
||||
baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
|
||||
endpoints: (builder) => ({
|
||||
// TODO :)
|
||||
getStatus: builder.query<MigrateToCloudStatusDTO, void>({
|
||||
queryFn: () => dataWithMockDelay({ enabled: true }),
|
||||
providesTags: ['stackDetails'],
|
||||
queryFn: () => {
|
||||
const responseData: MigrateToCloudStatusDTO = { enabled: HAS_STACK_DETAILS };
|
||||
if (STACK_URL) {
|
||||
responseData.stackURL = STACK_URL;
|
||||
}
|
||||
return dataWithMockDelay(responseData);
|
||||
},
|
||||
}),
|
||||
connectStack: builder.mutation<void, ConnectStackDTO>({
|
||||
invalidatesTags: ['stackDetails'],
|
||||
queryFn: async ({ stackURL }) => {
|
||||
HAS_STACK_DETAILS = true;
|
||||
STACK_URL = stackURL;
|
||||
return dataWithMockDelay(undefined);
|
||||
},
|
||||
}),
|
||||
disconnectStack: builder.mutation<void, void>({
|
||||
invalidatesTags: ['stackDetails'],
|
||||
queryFn: async () => {
|
||||
HAS_STACK_DETAILS = false;
|
||||
return dataWithMockDelay(undefined);
|
||||
},
|
||||
}),
|
||||
|
||||
createMigrationToken: builder.mutation<CreateMigrationTokenResponseDTO, void>({
|
||||
@ -85,6 +115,8 @@ export const migrateToCloudAPI = createApi({
|
||||
|
||||
export const {
|
||||
useGetStatusQuery,
|
||||
useConnectStackMutation,
|
||||
useDisconnectStackMutation,
|
||||
useCreateMigrationTokenMutation,
|
||||
useDeleteMigrationTokenMutation,
|
||||
useHasMigrationTokenQuery,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Modal, Button } from '@grafana/ui';
|
||||
import { Modal, Button, Text } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
interface Props {
|
||||
@ -24,10 +24,12 @@ export const DeleteMigrationTokenModal = ({ hideModal, onConfirm }: Props) => {
|
||||
title={t('migrate-to-cloud.migration-token.delete-modal-title', 'Delete migration token')}
|
||||
onDismiss={hideModal}
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.migration-token.delete-modal-body">
|
||||
If you've already used this token with a self-managed installation, that installation will no longer be
|
||||
able to upload content.
|
||||
</Trans>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="migrate-to-cloud.migration-token.delete-modal-body">
|
||||
If you've already used this token with a self-managed installation, that installation will no longer be
|
||||
able to upload content.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={hideModal}>
|
||||
<Trans i18nKey="migrate-to-cloud.migration-token.delete-modal-cancel">Cancel</Trans>
|
||||
|
@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Modal, Button, Text } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
interface Props {
|
||||
hideModal: () => void;
|
||||
onConfirm: () => Promise<{ data: void } | { error: unknown }>;
|
||||
}
|
||||
|
||||
export const DisconnectModal = ({ hideModal, onConfirm }: Props) => {
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
|
||||
const onConfirmDisconnect = async () => {
|
||||
setIsDisconnecting(true);
|
||||
await onConfirm();
|
||||
setIsDisconnecting(false);
|
||||
hideModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
title={t('migrate-to-cloud.disconnect-modal.title', 'Disconnect from cloud stack')}
|
||||
onDismiss={hideModal}
|
||||
>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="migrate-to-cloud.disconnect-modal.body">
|
||||
This will remove the migration token from this installation. If you wish to upload more resources in the
|
||||
future, you will need to enter a new migration token.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={hideModal}>
|
||||
<Trans i18nKey="migrate-to-cloud.disconnect-modal.cancel">Cancel</Trans>
|
||||
</Button>
|
||||
<Button disabled={isDisconnecting} onClick={onConfirmDisconnect}>
|
||||
{isDisconnecting
|
||||
? t('migrate-to-cloud.disconnect-modal.disconnecting', 'Disconnecting...')
|
||||
: t('migrate-to-cloud.disconnect-modal.disconnect', 'Disconnect')}
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Button, Text } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
export const CallToAction = () => {
|
||||
const onClickMigrate = () => {
|
||||
console.log('TODO migration!');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" padding={5} gap={2} direction="column" alignItems="center" backgroundColor="secondary">
|
||||
<Text variant="h3" textAlignment="center">
|
||||
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
|
||||
</Text>
|
||||
<Button onClick={onClickMigrate}>
|
||||
<Trans i18nKey="migrate-to-cloud.cta.button">Migrate this instance to Cloud</Trans>
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Button, ModalsController, Text } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { useConnectStackMutation, useGetStatusQuery } from '../../../api';
|
||||
|
||||
import { ConnectModal } from './ConnectModal';
|
||||
|
||||
export const CallToAction = () => {
|
||||
const [connectStack, connectResponse] = useConnectStackMutation();
|
||||
const { isFetching } = useGetStatusQuery();
|
||||
|
||||
return (
|
||||
<ModalsController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<Box display="flex" padding={5} gap={2} direction="column" alignItems="center" backgroundColor="secondary">
|
||||
<Text variant="h3" textAlignment="center">
|
||||
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
disabled={isFetching || connectResponse.isLoading}
|
||||
onClick={() =>
|
||||
showModal(ConnectModal, {
|
||||
hideModal,
|
||||
onConfirm: connectStack,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.cta.button">Migrate this instance to Cloud</Trans>
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</ModalsController>
|
||||
);
|
||||
};
|
@ -0,0 +1,128 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useId, useState } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Modal, Button, Stack, TextLink, Field, Input, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { ConnectStackDTO } from '../../../api';
|
||||
|
||||
interface Props {
|
||||
hideModal: () => void;
|
||||
onConfirm: (connectStackData: ConnectStackDTO) => Promise<{ data: void } | { error: unknown }>;
|
||||
}
|
||||
|
||||
export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const cloudStackId = useId();
|
||||
const tokenId = useId();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors },
|
||||
watch,
|
||||
} = useForm<ConnectStackDTO>({
|
||||
defaultValues: {
|
||||
stackURL: '',
|
||||
token: '',
|
||||
},
|
||||
});
|
||||
|
||||
const stackURL = watch('stackURL');
|
||||
const token = watch('token');
|
||||
|
||||
const onConfirmConnect: SubmitHandler<ConnectStackDTO> = async (formData) => {
|
||||
setIsConnecting(true);
|
||||
await onConfirm(formData);
|
||||
setIsConnecting(false);
|
||||
hideModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen title={t('migrate-to-cloud.connect-modal.title', 'Connect to a cloud stack')} onDismiss={hideModal}>
|
||||
<form onSubmit={handleSubmit(onConfirmConnect)}>
|
||||
<Text color="secondary">
|
||||
<Stack direction="column" gap={2} alignItems="flex-start">
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-get-started">
|
||||
To get started, you'll need a Grafana.com account.
|
||||
</Trans>
|
||||
<TextLink href="https://grafana.com/auth/sign-up/create-user?pg=prod-cloud" external>
|
||||
{t('migrate-to-cloud.connect-modal.body-sign-up', 'Sign up for a Grafana.com account')}
|
||||
</TextLink>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-cloud-stack">
|
||||
You'll also need a cloud stack. If you just signed up, we'll automatically create your first
|
||||
stack. If you have an account, you'll need to select or create a stack.
|
||||
</Trans>
|
||||
<TextLink href="https://grafana.com/auth/sign-in/" external>
|
||||
{t('migrate-to-cloud.connect-modal.body-view-stacks', 'View my cloud stacks')}
|
||||
</TextLink>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-paste-stack">
|
||||
Once you've decided on a stack, paste the URL below.
|
||||
</Trans>
|
||||
<Field
|
||||
className={styles.field}
|
||||
invalid={!!errors.stackURL}
|
||||
error={errors.stackURL?.message}
|
||||
label={t('migrate-to-cloud.connect-modal.body-url-field', 'Cloud stack URL')}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
{...register('stackURL', {
|
||||
required: t('migrate-to-cloud.connect-modal.stack-required-error', 'Stack URL is required'),
|
||||
})}
|
||||
id={cloudStackId}
|
||||
placeholder="https://example.grafana.net/"
|
||||
/>
|
||||
</Field>
|
||||
<span>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-token">
|
||||
Your self-managed Grafana installation needs special access to securely migrate content. You'll
|
||||
need to create a migration token on your chosen cloud stack.
|
||||
</Trans>
|
||||
</span>
|
||||
<span>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-token-instructions">
|
||||
Log into your cloud stack and navigate to Administration, General, Migrate to Grafana Cloud. Create a
|
||||
migration token on that screen and paste the token here.
|
||||
</Trans>
|
||||
</span>
|
||||
<Field
|
||||
className={styles.field}
|
||||
invalid={!!errors.token}
|
||||
error={errors.token?.message}
|
||||
label={t('migrate-to-cloud.connect-modal.body-token-field', 'Migration token')}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
{...register('token', {
|
||||
required: t('migrate-to-cloud.connect-modal.token-required-error', 'Migration token is required'),
|
||||
})}
|
||||
id={tokenId}
|
||||
placeholder={t('migrate-to-cloud.connect-modal.body-token-field-placeholder', 'Paste token here')}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
</Text>
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={hideModal}>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.cancel">Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" disabled={isConnecting || !(stackURL && token)}>
|
||||
{isConnecting
|
||||
? t('migrate-to-cloud.connect-modal.connecting', 'Connecting to this stack...')
|
||||
: t('migrate-to-cloud.connect-modal.connect', 'Connect to this stack')}
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
field: css({
|
||||
alignSelf: 'stretch',
|
||||
}),
|
||||
});
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Grid, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { CallToAction } from './CallToAction';
|
||||
import { CallToAction } from './CallToAction/CallToAction';
|
||||
import { InfoPaneLeft } from './InfoPaneLeft';
|
||||
import { InfoPaneRight } from './InfoPaneRight';
|
||||
|
||||
|
@ -1,8 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, ModalsController, Stack, Text } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { useDisconnectStackMutation, useGetStatusQuery } from '../api';
|
||||
|
||||
import { DisconnectModal } from './DisconnectModal';
|
||||
import { EmptyState } from './EmptyState/EmptyState';
|
||||
|
||||
export const Page = () => {
|
||||
// TODO logic to determine whether to show the empty state or the resource table
|
||||
return <EmptyState />;
|
||||
const { data, isFetching } = useGetStatusQuery();
|
||||
const [disconnectStack, disconnectResponse] = useDisconnectStackMutation();
|
||||
if (!data?.enabled) {
|
||||
return <EmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalsController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<Stack alignItems="center">
|
||||
{data.stackURL && <Text variant="h4">{data.stackURL}</Text>}
|
||||
<Button
|
||||
disabled={isFetching || disconnectResponse.isLoading}
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
showModal(DisconnectModal, {
|
||||
hideModal,
|
||||
onConfirm: disconnectStack,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.resources.disconnect">Disconnect</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</ModalsController>
|
||||
);
|
||||
};
|
||||
|
@ -697,10 +697,35 @@
|
||||
"link-title": "Learn about migrating other settings",
|
||||
"title": "Can I move this installation to Grafana Cloud?"
|
||||
},
|
||||
"connect-modal": {
|
||||
"body-cloud-stack": "You'll also need a cloud stack. If you just signed up, we'll automatically create your first stack. If you have an account, you'll need to select or create a stack.",
|
||||
"body-get-started": "To get started, you'll need a Grafana.com account.",
|
||||
"body-paste-stack": "Once you've decided on a stack, paste the URL below.",
|
||||
"body-sign-up": "Sign up for a Grafana.com account",
|
||||
"body-token": "Your self-managed Grafana installation needs special access to securely migrate content. You'll need to create a migration token on your chosen cloud stack.",
|
||||
"body-token-field": "Migration token",
|
||||
"body-token-field-placeholder": "Paste token here",
|
||||
"body-token-instructions": "Log into your cloud stack and navigate to Administration, General, Migrate to Grafana Cloud. Create a migration token on that screen and paste the token here.",
|
||||
"body-url-field": "Cloud stack URL",
|
||||
"body-view-stacks": "View my cloud stacks",
|
||||
"cancel": "Cancel",
|
||||
"connect": "Connect to this stack",
|
||||
"connecting": "Connecting to this stack...",
|
||||
"stack-required-error": "Stack URL is required",
|
||||
"title": "Connect to a cloud stack",
|
||||
"token-required-error": "Migration token is required"
|
||||
},
|
||||
"cta": {
|
||||
"button": "Migrate this instance to Cloud",
|
||||
"header": "Let us manage your Grafana stack"
|
||||
},
|
||||
"disconnect-modal": {
|
||||
"body": "This will remove the migration token from this installation. If you wish to upload more resources in the future, you will need to enter a new migration token.",
|
||||
"cancel": "Cancel",
|
||||
"disconnect": "Disconnect",
|
||||
"disconnecting": "Disconnecting...",
|
||||
"title": "Disconnect from cloud stack"
|
||||
},
|
||||
"get-started": {
|
||||
"body": "The migration process must be started from your self-managed Grafana instance.",
|
||||
"configure-pdc-link": "Configure PDC for this stack",
|
||||
@ -751,6 +776,9 @@
|
||||
"link-title": "Grafana Cloud pricing",
|
||||
"title": "How much does it cost?"
|
||||
},
|
||||
"resources": {
|
||||
"disconnect": "Disconnect"
|
||||
},
|
||||
"token-status": {
|
||||
"active": "Token created and active",
|
||||
"no-active": "No active token"
|
||||
|
@ -697,10 +697,35 @@
|
||||
"link-title": "Ŀęäřʼn äþőūŧ mįģřäŧįʼnģ őŧĥęř şęŧŧįʼnģş",
|
||||
"title": "Cäʼn Ĩ mővę ŧĥįş įʼnşŧäľľäŧįőʼn ŧő Ğřäƒäʼnä Cľőūđ?"
|
||||
},
|
||||
"connect-modal": {
|
||||
"body-cloud-stack": "Ÿőū'ľľ äľşő ʼnęęđ ä čľőūđ şŧäčĸ. Ĩƒ yőū ĵūşŧ şįģʼnęđ ūp, ŵę'ľľ äūŧőmäŧįčäľľy čřęäŧę yőūř ƒįřşŧ şŧäčĸ. Ĩƒ yőū ĥävę äʼn äččőūʼnŧ, yőū'ľľ ʼnęęđ ŧő şęľęčŧ őř čřęäŧę ä şŧäčĸ.",
|
||||
"body-get-started": "Ŧő ģęŧ şŧäřŧęđ, yőū'ľľ ʼnęęđ ä Ğřäƒäʼnä.čőm äččőūʼnŧ.",
|
||||
"body-paste-stack": "Øʼnčę yőū'vę đęčįđęđ őʼn ä şŧäčĸ, päşŧę ŧĥę ŮŖĿ þęľőŵ.",
|
||||
"body-sign-up": "Ŝįģʼn ūp ƒőř ä Ğřäƒäʼnä.čőm äččőūʼnŧ",
|
||||
"body-token": "Ÿőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäľľäŧįőʼn ʼnęęđş şpęčįäľ äččęşş ŧő şęčūřęľy mįģřäŧę čőʼnŧęʼnŧ. Ÿőū'ľľ ʼnęęđ ŧő čřęäŧę ä mįģřäŧįőʼn ŧőĸęʼn őʼn yőūř čĥőşęʼn čľőūđ şŧäčĸ.",
|
||||
"body-token-field": "Mįģřäŧįőʼn ŧőĸęʼn",
|
||||
"body-token-field-placeholder": "Päşŧę ŧőĸęʼn ĥęřę",
|
||||
"body-token-instructions": "Ŀőģ įʼnŧő yőūř čľőūđ şŧäčĸ äʼnđ ʼnävįģäŧę ŧő Åđmįʼnįşŧřäŧįőʼn, Ğęʼnęřäľ, Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ. Cřęäŧę ä mįģřäŧįőʼn ŧőĸęʼn őʼn ŧĥäŧ şčřęęʼn äʼnđ päşŧę ŧĥę ŧőĸęʼn ĥęřę.",
|
||||
"body-url-field": "Cľőūđ şŧäčĸ ŮŖĿ",
|
||||
"body-view-stacks": "Vįęŵ my čľőūđ şŧäčĸş",
|
||||
"cancel": "Cäʼnčęľ",
|
||||
"connect": "Cőʼnʼnęčŧ ŧő ŧĥįş şŧäčĸ",
|
||||
"connecting": "Cőʼnʼnęčŧįʼnģ ŧő ŧĥįş şŧäčĸ...",
|
||||
"stack-required-error": "Ŝŧäčĸ ŮŖĿ įş řęqūįřęđ",
|
||||
"title": "Cőʼnʼnęčŧ ŧő ä čľőūđ şŧäčĸ",
|
||||
"token-required-error": "Mįģřäŧįőʼn ŧőĸęʼn įş řęqūįřęđ"
|
||||
},
|
||||
"cta": {
|
||||
"button": "Mįģřäŧę ŧĥįş įʼnşŧäʼnčę ŧő Cľőūđ",
|
||||
"header": "Ŀęŧ ūş mäʼnäģę yőūř Ğřäƒäʼnä şŧäčĸ"
|
||||
},
|
||||
"disconnect-modal": {
|
||||
"body": "Ŧĥįş ŵįľľ řęmővę ŧĥę mįģřäŧįőʼn ŧőĸęʼn ƒřőm ŧĥįş įʼnşŧäľľäŧįőʼn. Ĩƒ yőū ŵįşĥ ŧő ūpľőäđ mőřę řęşőūřčęş įʼn ŧĥę ƒūŧūřę, yőū ŵįľľ ʼnęęđ ŧő ęʼnŧęř ä ʼnęŵ mįģřäŧįőʼn ŧőĸęʼn.",
|
||||
"cancel": "Cäʼnčęľ",
|
||||
"disconnect": "Đįşčőʼnʼnęčŧ",
|
||||
"disconnecting": "Đįşčőʼnʼnęčŧįʼnģ...",
|
||||
"title": "Đįşčőʼnʼnęčŧ ƒřőm čľőūđ şŧäčĸ"
|
||||
},
|
||||
"get-started": {
|
||||
"body": "Ŧĥę mįģřäŧįőʼn přőčęşş mūşŧ þę şŧäřŧęđ ƒřőm yőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäʼnčę.",
|
||||
"configure-pdc-link": "Cőʼnƒįģūřę PĐC ƒőř ŧĥįş şŧäčĸ",
|
||||
@ -751,6 +776,9 @@
|
||||
"link-title": "Ğřäƒäʼnä Cľőūđ přįčįʼnģ",
|
||||
"title": "Ħőŵ mūčĥ đőęş įŧ čőşŧ?"
|
||||
},
|
||||
"resources": {
|
||||
"disconnect": "Đįşčőʼnʼnęčŧ"
|
||||
},
|
||||
"token-status": {
|
||||
"active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę",
|
||||
"no-active": "Ńő äčŧįvę ŧőĸęʼn"
|
||||
|
Loading…
Reference in New Issue
Block a user