E2C: Start connecting on-prem to real apis (#85527)

* E2C: Start connecting on-prem to real apis

* actually run the migration

* show resources

* basic dashboards resources

* show dashboard title

* remove console logs

* cleanup 1

* i18n

* disconnect

* i18n

* restore type

* use fixed format

* fix
This commit is contained in:
Josh Hunt 2024-04-03 18:47:02 +01:00 committed by GitHub
parent 0ec48cfdbd
commit aad79c9400
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 403 additions and 74 deletions

View File

@ -175,7 +175,8 @@ type MigrateDataResponseDTO struct {
}
type MigrateDataResponseItemDTO struct {
RefID string `json:"refId"`
Status ItemStatus `json:"status"`
Error string `json:"error,omitempty"`
Type MigrateDataType `json:"type"`
RefID string `json:"refId"`
Status ItemStatus `json:"status"`
Error string `json:"error,omitempty"`
}

View File

@ -15963,9 +15963,15 @@
},
"status": {
"$ref": "#/definitions/ItemStatus"
},
"type": {
"$ref": "#/definitions/MigrateDataType"
}
}
},
"MigrateDataType": {
"type": "string"
},
"MoveFolderCommand": {
"description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.",
"type": "object",

View File

@ -6,6 +6,9 @@ import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
interface RequestOptions extends BackendSrvRequest {
manageError?: (err: unknown) => { error: unknown };
showErrorAlert?: boolean;
// rtk codegen sets this
body?: BackendSrvRequest['data'];
}
function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn<RequestOptions> {
@ -16,6 +19,7 @@ function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryF
...requestOptions,
url: baseURL + requestOptions.url,
showErrorAlert: requestOptions.showErrorAlert,
data: requestOptions.body,
})
);
return { data: responseData, meta };

View File

@ -25,10 +25,13 @@ const injectedRtkApi = api.injectEndpoints({
createCloudMigrationToken: build.mutation<CreateCloudMigrationTokenApiResponse, CreateCloudMigrationTokenApiArg>({
query: () => ({ url: `/cloudmigration/token`, method: 'POST' }),
}),
getDashboardByUid: build.query<GetDashboardByUidApiResponse, GetDashboardByUidApiArg>({
query: (queryArg) => ({ url: `/dashboards/uid/${queryArg.uid}` }),
}),
}),
overrideExisting: false,
});
export { injectedRtkApi as enhancedApi };
export { injectedRtkApi as generatedAPI };
export type GetMigrationListApiResponse = /** status 200 (empty) */ CloudMigrationListResponse;
export type GetMigrationListApiArg = void;
export type CreateMigrationApiResponse = /** status 200 (empty) */ CloudMigrationResponse;
@ -64,6 +67,10 @@ export type GetCloudMigrationRunApiArg = {
};
export type CreateCloudMigrationTokenApiResponse = /** status 200 (empty) */ CreateAccessTokenResponseDto;
export type CreateCloudMigrationTokenApiArg = void;
export type GetDashboardByUidApiResponse = /** status 200 (empty) */ DashboardFullWithMeta;
export type GetDashboardByUidApiArg = {
uid: string;
};
export type CloudMigrationResponse = {
created?: string;
id?: number;
@ -87,10 +94,12 @@ export type CloudMigrationRequest = {
authToken?: string;
};
export type ItemStatus = string;
export type MigrateDataType = string;
export type MigrateDataResponseItemDto = {
error?: string;
refId?: string;
status?: ItemStatus;
type?: MigrateDataType;
};
export type MigrateDataResponseDto = {
id?: number;
@ -102,6 +111,50 @@ export type CloudMigrationRunList = {
export type CreateAccessTokenResponseDto = {
token?: string;
};
export type Json = object;
export type AnnotationActions = {
canAdd?: boolean;
canDelete?: boolean;
canEdit?: boolean;
};
export type AnnotationPermission = {
dashboard?: AnnotationActions;
organization?: AnnotationActions;
};
export type DashboardMeta = {
annotationsPermissions?: AnnotationPermission;
canAdmin?: boolean;
canDelete?: boolean;
canEdit?: boolean;
canSave?: boolean;
canStar?: boolean;
created?: string;
createdBy?: string;
expires?: string;
/** Deprecated: use FolderUID instead */
folderId?: number;
folderTitle?: string;
folderUid?: string;
folderUrl?: string;
hasAcl?: boolean;
isFolder?: boolean;
isSnapshot?: boolean;
isStarred?: boolean;
provisioned?: boolean;
provisionedExternalId?: string;
publicDashboardEnabled?: boolean;
publicDashboardUid?: string;
slug?: string;
type?: string;
updated?: string;
updatedBy?: string;
url?: string;
version?: number;
};
export type DashboardFullWithMeta = {
dashboard?: Json;
meta?: DashboardMeta;
};
export const {
useGetMigrationListQuery,
useCreateMigrationMutation,
@ -111,4 +164,5 @@ export const {
useRunCloudMigrationMutation,
useGetCloudMigrationRunQuery,
useCreateCloudMigrationTokenMutation,
useGetDashboardByUidQuery,
} = injectedRtkApi;

View File

@ -1,2 +1,39 @@
export * from './endpoints.gen';
export { enhancedApi as cloudMigrationAPI } from './endpoints.gen';
import { generatedAPI } from './endpoints.gen';
export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
addTagTypes: ['cloud-migration-config', 'cloud-migration-run'],
endpoints: {
// List Cloud Configs
getMigrationList: {
providesTags: ['cloud-migration-config'] /* should this be a -list? */,
},
// Create Cloud Config
createMigration: {
invalidatesTags: ['cloud-migration-config'],
},
// Get one Cloud Config
getCloudMigration: {
providesTags: ['cloud-migration-config'],
},
// Delete one Cloud Config
deleteCloudMigration: {
invalidatesTags: ['cloud-migration-config'],
},
getCloudMigrationRunList: {
providesTags: ['cloud-migration-run'] /* should this be a -list? */,
},
getCloudMigrationRun: {
providesTags: ['cloud-migration-run'],
},
runCloudMigration: {
invalidatesTags: ['cloud-migration-run'],
},
},
});

View File

@ -41,11 +41,13 @@ export interface ConnectStackDTOMock {
token: string;
}
export interface MigrationResourceDTOMock {
type MigrationResourceStatus = 'not-migrated' | 'migrated' | 'migrating' | 'failed';
export interface MigrationResourceDatasource {
uid: string;
status: 'not-migrated' | 'migrated' | 'migrating' | 'failed';
status: MigrationResourceStatus;
statusMessage?: string;
type: 'datasource' | 'dashboard'; // TODO: in future this would be a discriminated union with the resource details
type: 'datasource';
resource: {
uid: string;
name: string;
@ -54,6 +56,19 @@ export interface MigrationResourceDTOMock {
};
}
export interface MigrationResourceDashboard {
uid: string;
status: MigrationResourceStatus;
statusMessage?: string;
type: 'dashboard';
resource: {
uid: string;
name: string;
};
}
export type MigrationResourceDTOMock = MigrationResourceDatasource | MigrationResourceDashboard;
const mockApplications = ['auth-service', 'web server', 'backend'];
const mockEnvs = ['DEV', 'PROD'];
const mockRoles = ['db', 'load-balancer', 'server', 'logs'];

View File

@ -3,13 +3,12 @@ import React from 'react';
import { Box, Button, ModalsController, Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { useConnectStackMutationMock, useGetStatusQueryMock } from '../../../mockAPI';
import { useCreateMigrationMutation } from '../../../api';
import { ConnectModal } from './ConnectModal';
export const CallToAction = () => {
const [connectStack, connectResponse] = useConnectStackMutationMock();
const { isFetching } = useGetStatusQueryMock();
const [createMigration, createMigrationResponse] = useCreateMigrationMutation();
return (
<ModalsController>
@ -19,11 +18,11 @@ export const CallToAction = () => {
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
</Text>
<Button
disabled={isFetching || connectResponse.isLoading}
disabled={createMigrationResponse.isLoading}
onClick={() =>
showModal(ConnectModal, {
hideModal,
onConfirm: connectStack,
onConfirm: createMigration,
})
}
>

View File

@ -6,16 +6,19 @@ 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 { ConnectStackDTOMock } from '../../../mockAPI';
import { CreateMigrationApiArg } from '../../../api';
interface Props {
hideModal: () => void;
onConfirm: (connectStackData: ConnectStackDTOMock) => Promise<{ data: void } | { error: unknown }>;
onConfirm: (connectStackData: CreateMigrationApiArg) => Promise<unknown>;
}
interface FormData {
token: string;
}
export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
const [isConnecting, setIsConnecting] = useState(false);
const cloudStackId = useId();
const tokenId = useId();
const styles = useStyles2(getStyles);
@ -24,19 +27,22 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
register,
formState: { errors },
watch,
} = useForm<ConnectStackDTOMock>({
} = useForm<FormData>({
defaultValues: {
stackURL: '',
token: '',
},
});
const stackURL = watch('stackURL');
const token = watch('token');
const onConfirmConnect: SubmitHandler<ConnectStackDTOMock> = async (formData) => {
const onConfirmConnect: SubmitHandler<FormData> = async (formData) => {
setIsConnecting(true);
await onConfirm(formData);
// TODO: location of this is kinda weird, making it tricky to handle errors from this.
await onConfirm({
cloudMigrationRequest: {
authToken: formData.token,
},
});
setIsConnecting(false);
hideModal();
};
@ -49,46 +55,34 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
<Trans i18nKey="migrate-to-cloud.connect-modal.body-get-started">
To get started, you&apos;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&apos;ll also need a cloud stack. If you just signed up, we&apos;ll automatically create your first
stack. If you have an account, you&apos;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&apos;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&apos;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}
@ -110,7 +104,7 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
<Button variant="secondary" onClick={hideModal}>
<Trans i18nKey="migrate-to-cloud.connect-modal.cancel">Cancel</Trans>
</Button>
<Button type="submit" disabled={isConnecting || !(stackURL && token)}>
<Button type="submit" disabled={isConnecting || !token}>
{isConnecting
? t('migrate-to-cloud.connect-modal.connecting', 'Connecting to this stack...')
: t('migrate-to-cloud.connect-modal.connect', 'Connect to this stack')}

View File

@ -15,6 +15,7 @@ export const EmptyState = () => {
<div className={styles.container}>
<Stack direction="column">
<CallToAction />
<Grid
alignItems="flex-start"
gap={1}

View File

@ -1,43 +1,139 @@
import { skipToken } from '@reduxjs/toolkit/query/react';
import React, { useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { config } from '@grafana/runtime';
import { Alert, Box, Button, Stack } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { useGetStatusQueryMock, useListResourcesQueryMock, useStartMigrationMutationMock } from '../mockAPI';
import {
MigrateDataResponseDto,
useDeleteCloudMigrationMutation,
useGetCloudMigrationRunListQuery,
useGetCloudMigrationRunQuery,
useGetMigrationListQuery,
useRunCloudMigrationMutation,
} from '../api';
import { MigrationResourceDTOMock } from '../mockAPI';
import { DisconnectModal } from './DisconnectModal';
import { EmptyState } from './EmptyState/EmptyState';
import { MigrationInfo } from './MigrationInfo';
import { ResourcesTable } from './ResourcesTable';
export const Page = () => {
const { data: status, isFetching } = useGetStatusQueryMock();
const { data: resources } = useListResourcesQueryMock(status?.enabled ? undefined : skipToken);
const [startMigration, { isLoading: startMigrationIsLoading, isError: startMigrationIsError }] =
useStartMigrationMutationMock();
const [isDisconnecting, setIsDisconnecting] = useState(false);
/**
* Here's how migrations work:
*
* A single on-prem instance can be configured to be migrated to multiple cloud instances.
* - GetMigrationList returns this the list of migration targets for the on prem instance
* - If GetMigrationList returns an empty list, then the empty state with a prompt to enter a token should be shown
* - The UI (at the moment) only shows the most recently created migration target (the last one returned from the API)
* and doesn't allow for others to be created
*
* A single on-prem migration 'target' (CloudMigrationResponse) can have multiple migration runs (CloudMigrationRun)
* - To list the migration resources:
* 1. call GetCloudMigratiopnRunList to list all runs
* 2. call GetCloudMigrationRun with the ID from first step to list the result of that migration
*/
if (!status?.enabled) {
function useGetLatestMigrationDestination() {
const result = useGetMigrationListQuery();
const latestMigration = result.data?.migrations?.[0];
return {
...result,
data: latestMigration,
};
}
function useGetLatestMigrationRun(migrationId?: number) {
const listResult = useGetCloudMigrationRunListQuery(migrationId ? { id: migrationId } : skipToken);
const latestMigrationRun = listResult.data?.runs?.[0];
const runResult = useGetCloudMigrationRunQuery(
latestMigrationRun?.id && migrationId ? { runId: latestMigrationRun.id, id: migrationId } : skipToken
);
return {
...runResult,
data: runResult.data,
error: listResult.error || runResult.error,
isError: listResult.isError || runResult.isError,
isLoading: listResult.isLoading || runResult.isLoading,
isFetching: listResult.isFetching || runResult.isFetching,
};
}
export const Page = () => {
const migrationDestination = useGetLatestMigrationDestination();
const lastMigrationRun = useGetLatestMigrationRun(migrationDestination.data?.id);
const [performRunMigration, runMigrationResult] = useRunCloudMigrationMutation();
const [performDisconnect, disconnectResult] = useDeleteCloudMigrationMutation();
// isBusy is not a loading state, but indicates that the system is doing *something*
// and all buttons should be disabled
const isBusy =
runMigrationResult.isLoading ||
migrationDestination.isFetching ||
lastMigrationRun.isFetching ||
disconnectResult.isLoading;
const resources = useFixResources(lastMigrationRun.data);
const handleDisconnect = useCallback(() => {
if (migrationDestination.data?.id) {
performDisconnect({
id: migrationDestination.data.id,
});
}
}, [migrationDestination.data?.id, performDisconnect]);
const handleStartMigration = useCallback(() => {
if (migrationDestination.data?.id) {
performRunMigration({ id: migrationDestination.data?.id });
}
}, [performRunMigration, migrationDestination]);
const migrationMeta = migrationDestination.data;
const isInitialLoading = migrationDestination.isLoading;
if (isInitialLoading) {
// TODO: better loading state
return <div>Loading...</div>;
} else if (!migrationMeta) {
return <EmptyState />;
}
const isBusy = isFetching || isDisconnecting || startMigrationIsLoading;
return (
<>
<Stack direction="column" gap={4}>
{startMigrationIsError && (
{runMigrationResult.isError && (
<Alert
severity="error"
title={t(
'migrate-to-cloud.summary.error-starting-migration',
'There was an error starting cloud migration'
'migrate-to-cloud.summary.run-migration-error-title',
'There was an error migrating your resources'
)}
/>
>
<Trans i18nKey="migrate-to-cloud.summary.run-migration-error-description">
See the Grafana server logs for more details
</Trans>
</Alert>
)}
{status.stackURL && (
{disconnectResult.isError && (
<Alert
severity="error"
title={t('migrate-to-cloud.summary.disconnect-error-title', 'There was an error disconnecting')}
>
<Trans i18nKey="migrate-to-cloud.summary.disconnect-error-description">
See the Grafana server logs for more details
</Trans>
</Alert>
)}
{migrationMeta.stack && (
<Box
borderColor="weak"
borderStyle="solid"
@ -51,8 +147,14 @@ export const Page = () => {
title={t('migrate-to-cloud.summary.target-stack-title', 'Uploading to')}
value={
<>
{status.stackURL}{' '}
<Button onClick={() => setIsDisconnecting(true)} disabled={isBusy} variant="secondary" size="sm">
{migrationMeta.stack}{' '}
<Button
disabled={isBusy}
onClick={handleDisconnect}
variant="secondary"
size="sm"
icon={disconnectResult.isLoading ? 'spinner' : undefined}
>
<Trans i18nKey="migrate-to-cloud.summary.disconnect">Disconnect</Trans>
</Button>
</>
@ -61,8 +163,8 @@ export const Page = () => {
<Button
disabled={isBusy}
onClick={() => startMigration()}
icon={startMigrationIsLoading ? 'spinner' : undefined}
onClick={handleStartMigration}
icon={runMigrationResult.isLoading ? 'spinner' : undefined}
>
<Trans i18nKey="migrate-to-cloud.summary.start-migration">Upload everything</Trans>
</Button>
@ -72,7 +174,65 @@ export const Page = () => {
{resources && <ResourcesTable resources={resources} />}
</Stack>
<DisconnectModal isOpen={isDisconnecting} onDismiss={() => setIsDisconnecting(false)} />
{/* <DisconnectModal isOpen={isDisconnecting} onDismiss={() => setIsDisconnecting(false)} /> */}
</>
);
};
// converts API status to our expected/mocked status
function convertStatus(status: string) {
switch (status) {
case 'OK':
return 'migrated';
case 'ERROR':
return 'failed';
case 'failed':
return 'failed';
default:
return 'failed';
}
}
function useFixResources(data: MigrateDataResponseDto | undefined) {
return useMemo(() => {
if (!data?.items) {
return undefined;
}
const betterResources: MigrationResourceDTOMock[] = data.items.flatMap((item) => {
if (item.type === 'DATASOURCE') {
const datasourceConfig = Object.values(config.datasources).find((v) => v.uid === item.refId);
return {
type: 'datasource',
uid: item.refId ?? '',
status: convertStatus(item.status ?? ''),
statusMessage: item.error,
resource: {
uid: item.refId ?? '',
name: datasourceConfig?.name ?? 'Unknown data source',
type: datasourceConfig?.meta?.name ?? 'Unknown type',
icon: datasourceConfig?.meta?.info?.logos?.small,
},
};
}
if (item.type === 'DASHBOARD') {
return {
type: 'dashboard',
uid: item.refId ?? '',
status: convertStatus(item.status ?? ''),
statusMessage: item.error,
resource: {
uid: item.refId ?? '',
name: item.refId ?? 'Unknown dashboard',
},
};
}
return [];
});
return betterResources;
}, [data]);
}

View File

@ -1,11 +1,13 @@
import { css } from '@emotion/css';
import React from 'react';
import React, { useMemo } from 'react';
import Skeleton from 'react-loading-skeleton';
import { InteractiveTable, CellProps, Stack, Text, Icon, useStyles2, Button } from '@grafana/ui';
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
import { t } from 'app/core/internationalization';
import { MigrationResourceDTOMock } from '../mockAPI';
import { useGetDashboardByUidQuery } from '../api';
import { MigrationResourceDTOMock, MigrationResourceDashboard, MigrationResourceDatasource } from '../mockAPI';
interface ResourcesTableProps {
resources: MigrationResourceDTOMock[];
@ -23,18 +25,66 @@ export function ResourcesTable({ resources }: ResourcesTableProps) {
function NameCell(props: CellProps<MigrationResourceDTOMock>) {
const data = props.row.original;
return (
<Stack direction="row" gap={2} alignItems="center">
<ResourceIcon resource={data} />
<Stack direction="column" gap={0}>
<span>{data.resource.name}</span>
<Text color="secondary">{data.resource.type}</Text>
{data.type === 'datasource' ? <DatasourceInfo data={data} /> : <DashboardInfo data={data} />}
</Stack>
</Stack>
);
}
function getDashboardTitle(dashboardData: object) {
if ('title' in dashboardData && typeof dashboardData.title === 'string') {
return dashboardData.title;
}
return undefined;
}
function DatasourceInfo({ data }: { data: MigrationResourceDatasource }) {
return (
<>
<span>{data.resource.name}</span>
<Text color="secondary">{data.resource.type}</Text>
</>
);
}
// TODO: really, the API should return this directly
function DashboardInfo({ data }: { data: MigrationResourceDashboard }) {
const { data: dashboardData } = useGetDashboardByUidQuery({
uid: data.resource.uid,
});
const dashboardName = useMemo(() => {
return (dashboardData?.dashboard && getDashboardTitle(dashboardData.dashboard)) ?? data.resource.uid;
}, [dashboardData, data.resource.uid]);
if (!dashboardData) {
return (
<>
<span>
<Skeleton width={250} />
</span>
<span>
<Skeleton width={130} />
</span>
</>
);
}
return (
<>
<span>{dashboardName}</span>
<Text color="secondary">{dashboardData.meta?.folderTitle ?? 'Dashboards'}</Text>
</>
);
}
function TypeCell(props: CellProps<MigrationResourceDTOMock>) {
const { type } = props.row.original;

View File

@ -703,18 +703,15 @@
"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"
},
@ -796,7 +793,10 @@
},
"summary": {
"disconnect": "Disconnect",
"error-starting-migration": "There was an error starting cloud migration",
"disconnect-error-description": "See the Grafana server logs for more details",
"disconnect-error-title": "There was an error disconnecting",
"run-migration-error-description": "See the Grafana server logs for more details",
"run-migration-error-title": "There was an error migrating your resources",
"start-migration": "Upload everything",
"target-stack-title": "Uploading to"
},

View File

@ -703,18 +703,15 @@
"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ūįřęđ"
},
@ -796,7 +793,10 @@
},
"summary": {
"disconnect": "Đįşčőʼnʼnęčŧ",
"error-starting-migration": "Ŧĥęřę ŵäş äʼn ęřřőř şŧäřŧįʼnģ čľőūđ mįģřäŧįőʼn",
"disconnect-error-description": "Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş",
"disconnect-error-title": "Ŧĥęřę ŵäş äʼn ęřřőř đįşčőʼnʼnęčŧįʼnģ",
"run-migration-error-description": "Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş",
"run-migration-error-title": "Ŧĥęřę ŵäş äʼn ęřřőř mįģřäŧįʼnģ yőūř řęşőūřčęş",
"start-migration": "Ůpľőäđ ęvęřyŧĥįʼnģ",
"target-stack-title": "Ůpľőäđįʼnģ ŧő"
},

View File

@ -6740,10 +6740,16 @@
},
"status": {
"$ref": "#/components/schemas/ItemStatus"
},
"type": {
"$ref": "#/components/schemas/MigrateDataType"
}
},
"type": "object"
},
"MigrateDataType": {
"type": "string"
},
"MoveFolderCommand": {
"description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.",
"properties": {

View File

@ -5,6 +5,7 @@ const config: ConfigFile = {
schemaFile: '../public/openapi3.json',
apiFile: '', // leave this empty, and instead populate the outputFiles object below
hooks: true,
exportName: 'generatedAPI',
outputFiles: {
'../public/app/features/migrate-to-cloud/api/endpoints.gen.ts': {
@ -19,6 +20,7 @@ const config: ConfigFile = {
'getCloudMigrationRun',
'getCloudMigrationRunList',
'deleteCloudMigration',
'getDashboardByUid',
],
},
},