E2C: Implement cloud auth flow (#83409)

* implement cloud auth

* move logic into MigrationTokenPane folder

* update PDC link

* add missed translations
This commit is contained in:
Ashley Harrison 2024-02-27 09:54:06 +00:00 committed by GitHub
parent 2540842c95
commit faaf4dc1e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 273 additions and 36 deletions

View File

@ -31,7 +31,17 @@ interface MigrateToCloudStatusDTO {
enabled: boolean;
}
interface CreateMigrationTokenResponseDTO {
token: string;
}
// TODO remove these mock properties/functions
const MOCK_DELAY_MS = 1000;
const MOCK_TOKEN = 'TODO_thisWillBeABigLongToken';
let HAS_MIGRATION_TOKEN = false;
export const migrateToCloudAPI = createApi({
tagTypes: ['migrationToken'],
reducerPath: 'migrateToCloudAPI',
baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
endpoints: (builder) => ({
@ -39,7 +49,44 @@ export const migrateToCloudAPI = createApi({
getStatus: builder.query<MigrateToCloudStatusDTO, void>({
queryFn: () => ({ data: { enabled: false } }),
}),
createMigrationToken: builder.mutation<CreateMigrationTokenResponseDTO, void>({
invalidatesTags: ['migrationToken'],
queryFn: async () => {
return new Promise((resolve) => {
setTimeout(() => {
HAS_MIGRATION_TOKEN = true;
resolve({ data: { token: MOCK_TOKEN } });
}, MOCK_DELAY_MS);
});
},
}),
deleteMigrationToken: builder.mutation<void, void>({
invalidatesTags: ['migrationToken'],
queryFn: async () => {
return new Promise((resolve) => {
setTimeout(() => {
HAS_MIGRATION_TOKEN = false;
resolve({ data: undefined });
}, MOCK_DELAY_MS);
});
},
}),
hasMigrationToken: builder.query<boolean, void>({
providesTags: ['migrationToken'],
queryFn: async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ data: HAS_MIGRATION_TOKEN });
}, MOCK_DELAY_MS);
});
},
}),
}),
});
export const { useGetStatusQuery } = migrateToCloudAPI;
export const {
useGetStatusQuery,
useCreateMigrationTokenMutation,
useDeleteMigrationTokenMutation,
useHasMigrationTokenQuery,
} = migrateToCloudAPI;

View File

@ -61,7 +61,7 @@ export const InfoPane = () => {
</ol>
</Stack>
</InfoItem>
<TextLink href="/TODO">
<TextLink href="/connections/private-data-source-connections">
{t('migrate-to-cloud.get-started.configure-pdc-link', 'Configure PDC for this stack')}
</TextLink>
</Box>

View File

@ -1,30 +0,0 @@
import React from 'react';
import { Box, Button, Text } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { InfoItem } from '../shared/InfoItem';
export const MigrationTokenPane = () => {
const onGenerateToken = () => {
console.log('TODO: generate token!');
};
const tokenStatus = 'TODO';
return (
<Box display="flex" alignItems="flex-start" padding={2} gap={2} direction="column" backgroundColor="secondary">
<InfoItem title={t('migrate-to-cloud.migration-token.title', 'Migration token')}>
<Trans i18nKey="migrate-to-cloud.migration-token.body">
Your self-managed Grafana instance will require a special authentication token to securely connect to this
cloud stack.
</Trans>
</InfoItem>
<Text color="secondary">
<Trans i18nKey="migrate-to-cloud.migration-token.status">Current status: {{ tokenStatus }}</Trans>
</Text>
<Button onClick={onGenerateToken}>
<Trans i18nKey="migrate-to-cloud.migration-token.generate-button">Generate a migration token</Trans>
</Button>
</Box>
);
};

View File

@ -0,0 +1,43 @@
import React, { useState } from 'react';
import { Modal, Button } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
interface Props {
hideModal: () => void;
onConfirm: () => Promise<{ data: void } | { error: unknown }>;
}
export const DeleteMigrationTokenModal = ({ hideModal, onConfirm }: Props) => {
const [isDeleting, setIsDeleting] = useState(false);
const onConfirmDelete = async () => {
setIsDeleting(true);
await onConfirm();
setIsDeleting(false);
hideModal();
};
return (
<Modal
isOpen
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&apos;ve already used this token with a self-managed installation, that installation will no longer be
able to upload content.
</Trans>
<Modal.ButtonRow>
<Button variant="secondary" onClick={hideModal}>
<Trans i18nKey="migrate-to-cloud.migration-token.delete-modal-cancel">Cancel</Trans>
</Button>
<Button disabled={isDeleting} variant="destructive" onClick={onConfirmDelete}>
{isDeleting
? t('migrate-to-cloud.migration-token.delete-modal-deleting', 'Deleting...')
: t('migrate-to-cloud.migration-token.delete-modal-confirm', 'Delete')}
</Button>
</Modal.ButtonRow>
</Modal>
);
};

View File

@ -0,0 +1,45 @@
import React, { useId } from 'react';
import { Modal, Button, Input, Stack, ClipboardButton, Field } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
interface Props {
hideModal: () => void;
migrationToken: string;
}
export const MigrationTokenModal = ({ hideModal, migrationToken }: Props) => {
const inputId = useId();
return (
<Modal
isOpen
title={t('migrate-to-cloud.migration-token.modal-title', 'Migration token created')}
onDismiss={hideModal}
>
<Field
description={t(
'migrate-to-cloud.migration-token.modal-field-description',
'Copy the token now as you will not be able to see it again. Losing a token requires creating a new one.'
)}
htmlFor={inputId}
label={t('migrate-to-cloud.migration-token.modal-field-label', 'Token')}
>
<Stack>
<Input id={inputId} value={migrationToken} readOnly />
<ClipboardButton icon="clipboard-alt" getText={() => migrationToken}>
<Trans i18nKey="migrate-to-cloud.migration-token.modal-copy-button">Copy to clipboard</Trans>
</ClipboardButton>
</Stack>
</Field>
<Modal.ButtonRow>
<Button variant="secondary" onClick={hideModal}>
<Trans i18nKey="migrate-to-cloud.migration-token.modal-close">Close</Trans>
</Button>
<ClipboardButton variant="primary" getText={() => migrationToken} onClipboardCopy={hideModal}>
<Trans i18nKey="migrate-to-cloud.migration-token.modal-copy-and-close">Copy to clipboard and close</Trans>
</ClipboardButton>
</Modal.ButtonRow>
</Modal>
);
};

View File

@ -0,0 +1,72 @@
import React from 'react';
import { Box, Button, ModalsController, Text } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { useCreateMigrationTokenMutation, useDeleteMigrationTokenMutation, useHasMigrationTokenQuery } from '../../api';
import { InfoItem } from '../../shared/InfoItem';
import { DeleteMigrationTokenModal } from './DeleteMigrationTokenModal';
import { MigrationTokenModal } from './MigrationTokenModal';
import { TokenStatus } from './TokenStatus';
export const MigrationTokenPane = () => {
const { data: hasToken, isFetching } = useHasMigrationTokenQuery();
const [createToken, createTokenResponse] = useCreateMigrationTokenMutation();
const [deleteToken, deleteTokenResponse] = useDeleteMigrationTokenMutation();
return (
<ModalsController>
{({ showModal, hideModal }) => (
<Box display="flex" alignItems="flex-start" padding={2} gap={2} direction="column" backgroundColor="secondary">
<InfoItem title={t('migrate-to-cloud.migration-token.title', 'Migration token')}>
<Trans i18nKey="migrate-to-cloud.migration-token.body">
Your self-managed Grafana instance will require a special authentication token to securely connect to this
cloud stack.
</Trans>
</InfoItem>
<Text color="secondary">
<Trans i18nKey="migrate-to-cloud.migration-token.status">
Current status:{' '}
<TokenStatus
hasToken={Boolean(hasToken)}
isFetching={isFetching || createTokenResponse.isLoading || deleteTokenResponse.isLoading}
/>
</Trans>
</Text>
{hasToken ? (
<Button
variant="destructive"
onClick={() =>
showModal(DeleteMigrationTokenModal, {
hideModal,
onConfirm: deleteToken,
})
}
disabled={isFetching || deleteTokenResponse.isLoading}
>
<Trans i18nKey="migrate-to-cloud.migration-token.delete-button">Delete this migration token</Trans>
</Button>
) : (
<Button
disabled={createTokenResponse.isLoading || isFetching}
onClick={async () => {
const response = await createToken();
if ('data' in response) {
showModal(MigrationTokenModal, {
hideModal,
migrationToken: response.data.token,
});
}
}}
>
{createTokenResponse.isLoading
? t('migrate-to-cloud.migration-token.generate-button-loading', 'Generating a migration token...')
: t('migrate-to-cloud.migration-token.generate-button', 'Generate a migration token')}
</Button>
)}
</Box>
)}
</ModalsController>
);
};

View File

@ -0,0 +1,24 @@
import React from 'react';
import Skeleton from 'react-loading-skeleton';
import { Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
interface Props {
hasToken: boolean;
isFetching: boolean;
}
export const TokenStatus = ({ hasToken, isFetching }: Props) => {
if (isFetching) {
return <Skeleton width={100} />;
}
return hasToken ? (
<Text color="success">
<Trans i18nKey="migrate-to-cloud.token-status.active">Token created and active</Trans>
</Text>
) : (
<Trans i18nKey="migrate-to-cloud.token-status.no-active">No active token</Trans>
);
};

View File

@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Grid, useStyles2 } from '@grafana/ui';
import { InfoPane } from './InfoPane';
import { MigrationTokenPane } from './MigrationTokenPane';
import { MigrationTokenPane } from './MigrationTokenPane/MigrationTokenPane';
export const Page = () => {
const styles = useStyles2(getStyles);

View File

@ -1,6 +1,7 @@
import { configureStore as reduxConfigureStore, createListenerMiddleware } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { migrateToCloudAPI } from 'app/features/admin/migrate-to-cloud/api';
import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi';
import { StoreState } from 'app/types/store';
@ -28,7 +29,8 @@ export function configureStore(initialState?: Partial<StoreState>) {
listenerMiddleware.middleware,
alertingApi.middleware,
publicDashboardApi.middleware,
browseDashboardsAPI.middleware
browseDashboardsAPI.middleware,
migrateToCloudAPI.middleware
),
devTools: process.env.NODE_ENV !== 'production',
preloadedState: {

View File

@ -724,8 +724,21 @@
},
"migration-token": {
"body": "Your self-managed Grafana instance will require a special authentication token to securely connect to this cloud stack.",
"delete-button": "Delete this 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.",
"delete-modal-cancel": "Cancel",
"delete-modal-confirm": "Delete",
"delete-modal-deleting": "Deleting...",
"delete-modal-title": "Delete migration token",
"generate-button": "Generate a migration token",
"status": "Current status: {{tokenStatus}}",
"generate-button-loading": "Generating a migration token...",
"modal-close": "Close",
"modal-copy-and-close": "Copy to clipboard and close",
"modal-copy-button": "Copy to clipboard",
"modal-field-description": "Copy the token now as you will not be able to see it again. Losing a token requires creating a new one.",
"modal-field-label": "Token",
"modal-title": "Migration token created",
"status": "Current status: <2></2>",
"title": "Migration token"
},
"pdc": {
@ -738,6 +751,10 @@
"link-title": "Grafana Cloud pricing",
"title": "How much does it cost?"
},
"token-status": {
"active": "Token created and active",
"no-active": "No active token"
},
"what-is-cloud": {
"body": "Grafana cloud is a fully managed cloud-hosted observability platform ideal for cloud native environments. It's everything you love about Grafana without the overhead of maintaining, upgrading, and supporting an installation.",
"link-title": "Learn about cloud features",

View File

@ -724,8 +724,21 @@
},
"migration-token": {
"body": "Ÿőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäʼnčę ŵįľľ řęqūįřę ä şpęčįäľ äūŧĥęʼnŧįčäŧįőʼn ŧőĸęʼn ŧő şęčūřęľy čőʼnʼnęčŧ ŧő ŧĥįş čľőūđ şŧäčĸ.",
"delete-button": "Đęľęŧę ŧĥįş mįģřäŧįőʼn ŧőĸęʼn",
"delete-modal-body": "Ĩƒ yőū'vę äľřęäđy ūşęđ ŧĥįş ŧőĸęʼn ŵįŧĥ ä şęľƒ-mäʼnäģęđ įʼnşŧäľľäŧįőʼn, ŧĥäŧ įʼnşŧäľľäŧįőʼn ŵįľľ ʼnő ľőʼnģęř þę äþľę ŧő ūpľőäđ čőʼnŧęʼnŧ.",
"delete-modal-cancel": "Cäʼnčęľ",
"delete-modal-confirm": "Đęľęŧę",
"delete-modal-deleting": "Đęľęŧįʼnģ...",
"delete-modal-title": "Đęľęŧę mįģřäŧįőʼn ŧőĸęʼn",
"generate-button": "Ğęʼnęřäŧę ä mįģřäŧįőʼn ŧőĸęʼn",
"status": "Cūřřęʼnŧ şŧäŧūş: {{tokenStatus}}",
"generate-button-loading": "Ğęʼnęřäŧįʼnģ ä mįģřäŧįőʼn ŧőĸęʼn...",
"modal-close": "Cľőşę",
"modal-copy-and-close": "Cőpy ŧő čľįpþőäřđ äʼnđ čľőşę",
"modal-copy-button": "Cőpy ŧő čľįpþőäřđ",
"modal-field-description": "Cőpy ŧĥę ŧőĸęʼn ʼnőŵ äş yőū ŵįľľ ʼnőŧ þę äþľę ŧő şęę įŧ äģäįʼn. Ŀőşįʼnģ ä ŧőĸęʼn řęqūįřęş čřęäŧįʼnģ ä ʼnęŵ őʼnę.",
"modal-field-label": "Ŧőĸęʼn",
"modal-title": "Mįģřäŧįőʼn ŧőĸęʼn čřęäŧęđ",
"status": "Cūřřęʼnŧ şŧäŧūş: <2></2>",
"title": "Mįģřäŧįőʼn ŧőĸęʼn"
},
"pdc": {
@ -738,6 +751,10 @@
"link-title": "Ğřäƒäʼnä Cľőūđ přįčįʼnģ",
"title": "Ħőŵ mūčĥ đőęş įŧ čőşŧ?"
},
"token-status": {
"active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę",
"no-active": "Ńő äčŧįvę ŧőĸęʼn"
},
"what-is-cloud": {
"body": "Ğřäƒäʼnä čľőūđ įş ä ƒūľľy mäʼnäģęđ čľőūđ-ĥőşŧęđ őþşęřväþįľįŧy pľäŧƒőřm įđęäľ ƒőř čľőūđ ʼnäŧįvę ęʼnvįřőʼnmęʼnŧş. Ĩŧ'ş ęvęřyŧĥįʼnģ yőū ľővę äþőūŧ Ğřäƒäʼnä ŵįŧĥőūŧ ŧĥę ővęřĥęäđ őƒ mäįʼnŧäįʼnįʼnģ, ūpģřäđįʼnģ, äʼnđ şūppőřŧįʼnģ äʼn įʼnşŧäľľäŧįőʼn.",
"link-title": "Ŀęäřʼn äþőūŧ čľőūđ ƒęäŧūřęş",