mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 10:20:29 -06:00
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:
parent
2540842c95
commit
faaf4dc1e3
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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'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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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);
|
||||
|
@ -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: {
|
||||
|
@ -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",
|
||||
|
@ -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 äþőūŧ čľőūđ ƒęäŧūřęş",
|
||||
|
Loading…
Reference in New Issue
Block a user