Skip to content

Commit

Permalink
Cloud Migration FE (#42542)
Browse files Browse the repository at this point in the history
* it's a start

* ui wip

* wip

* dynamic polling intervals, and custom modal for migrate confirmation modal

* cleans out most of the remainig UI TODOs

* adding progress component

* impls team feedback

* makes component more testable, starts some a unit test for the CloudPanel

* finish unit testing

* reverts api changes

* update progress styling

* fix type issues

* fix e2e failure, fix feature unit tests by holding last migration state in fetchMock if more requests than expected happen at the end of a test, remove white spacing change in clj file

* second pass at fixing tests

* fix copy from ready-only to read-only

* copy fix

* Update frontend/src/metabase/admin/settings/components/CloudPanel/MigrationError.tsx

Co-authored-by: Raphael Krut-Landau <raphael.kl@gmail.com>

* Update frontend/src/metabase/admin/settings/components/CloudPanel/MigrationInProgress.tsx

Co-authored-by: Raphael Krut-Landau <raphael.kl@gmail.com>

* adding e2e test

* pr feedback

---------

Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
Co-authored-by: Raphael Krut-Landau <raphael.kl@gmail.com>
  • Loading branch information
3 people authored and noahmoss committed May 16, 2024
1 parent bb42b3c commit 8296891
Show file tree
Hide file tree
Showing 27 changed files with 980 additions and 10 deletions.
23 changes: 14 additions & 9 deletions e2e/test/scenarios/admin/settings/cloud.cy.spec.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { restore, setupMetabaseCloud } from "e2e/support/helpers";
import { restore, setTokenFeatures } from "e2e/support/helpers";

// Unskip when mocking Cloud in Cypress is fixed (#18289)
describe.skip("Cloud settings section", () => {
describe("Cloud settings section", () => {
beforeEach(() => {
restore();
cy.signInAsAdmin();
});

it("should be visible when running Metabase Cloud", () => {
setupMetabaseCloud();
// Setting to none will give us an instance where token-features.hosting is set to true
// Allowing us to pretend that we are a hosted instance (seems backwards though haha)

setTokenFeatures("none");
cy.visit("/admin");
cy.findByTestId("admin-list-settings-items").findByText("Cloud").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
Expand All @@ -21,13 +24,15 @@ describe.skip("Cloud settings section", () => {
);
});

it("should be invisible when self-hosting", () => {
it("should prompt us to migrate to cloud if we are not hosted", () => {
setTokenFeatures("all");
cy.visit("/admin");
cy.findByTestId("admin-list-settings-items")
.findByText("Cloud")
.should("not.exist");
cy.visit("/admin/settings/cloud");
cy.findByTestId("admin-list-settings-items").findByText("Cloud").click();

cy.location("pathname").should("contain", "/admin/settings/cloud");

// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(/Cloud Settings/i).should("not.exist");
cy.findByText(/Migrate to Cloud/i).should("exist");
cy.button("Get started").should("exist");
});
});
2 changes: 1 addition & 1 deletion e2e/test/scenarios/admin/settings/settings.cy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ describe("scenarios > admin > settings", () => {
() => {
isEE && setTokenFeatures("all");

const lastItem = isOSS ? "Caching" : "Appearance";
const lastItem = isOSS ? "Cloud" : "Appearance";

cy.visit("/admin/settings/setup");
cy.findByTestId("admin-list-settings-items").within(() => {
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/metabase-types/api/cloud-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type CloudMigrationState =
| "init"
| "setup"
| "dump"
| "upload"
| "cancelled"
| "error"
| "done";

export type CloudMigration = {
id: number;
external_id: string;
state: CloudMigrationState;
progress: number;
upload_url: string;
created_at: string;
updated_at: string;
};
1 change: 1 addition & 0 deletions frontend/src/metabase-types/api/mocks/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export const createMockSettings = (
"password-complexity": { total: 6, digit: 1 },
"persisted-models-enabled": false,
"premium-embedding-token": null,
"read-only-mode": false,
"report-timezone-short": "UTC",
"report-timezone-long": "Europe/London",
"saml-configured": false,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/metabase-types/api/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ interface InstanceSettings {
"enable-public-sharing": boolean;
"enable-xrays": boolean;
"example-dashboard-id": number | null;
"read-only-mode": boolean;
"search-typeahead-enabled": boolean;
"show-homepage-data": boolean;
"show-homepage-pin-message": boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import styled from "@emotion/styled";

import { color, alpha } from "metabase/lib/colors";

export const MigrationCard = styled.div`
border: 1px solid ${color("border")};
border-radius: 0.5rem;
padding: 2rem 3rem;
`;

export const LargeIconContainer = styled.div<{
color: string;
}>`
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
height: 4rem;
width: 4rem;
border-radius: 50%;
background: ${props => alpha(props.color, 0.15)};
color: ${props => props.color};
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useEffect, useState } from "react";
import { t } from "ttag";

import {
useGetCloudMigrationQuery,
useCreateCloudMigrationMutation,
} from "metabase/api";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import { useDispatch } from "metabase/lib/redux";
import { refreshSiteSettings } from "metabase/redux/settings";
import { Box, Text } from "metabase/ui";
import type { CloudMigration } from "metabase-types/api/cloud-migration";

import { MigrationError } from "./MigrationError";
import { MigrationInProgress } from "./MigrationInProgress";
import { MigrationStart } from "./MigrationStart";
import { MigrationSuccess } from "./MigrationSuccess";
import {
type InternalCloudMigrationState,
isInProgressMigration,
openCheckoutInNewTab,
getStartedVisibleStates,
defaultGetPollingInterval,
} from "./utils";

interface CloudPanelProps {
getPollingInterval: (migration: CloudMigration) => number | undefined;
onMigrationStart: (migration: CloudMigration) => void;
}

export const CloudPanel = ({
getPollingInterval = defaultGetPollingInterval,
onMigrationStart = openCheckoutInNewTab,
}: CloudPanelProps) => {
const dispatch = useDispatch();
const [pollingInterval, setPollingInterval] = useState<number | undefined>(
undefined,
);

const {
data: migration,
isLoading,
error,
} = useGetCloudMigrationQuery(undefined, {
refetchOnMountOrArgChange: true,
pollingInterval,
});

const migrationState: InternalCloudMigrationState =
migration?.state ?? "uninitialized";

useEffect(
function syncPollingInterval() {
if (migration) {
setPollingInterval(getPollingInterval(migration));
}
},
[migration, getPollingInterval],
);

useEffect(
function syncSiteSettings() {
if (migrationState) {
dispatch(refreshSiteSettings({}));
}
},
[dispatch, migrationState],
);

const [createCloudMigration, createCloudMigrationResult] =
useCreateCloudMigrationMutation();

const handleCreateMigration = async () => {
const migration = await createCloudMigration().unwrap();
await dispatch(refreshSiteSettings({}));
onMigrationStart(migration);
};

return (
<LoadingAndErrorWrapper loading={isLoading} error={error}>
<Box maw="30rem">
<Text fw="bold" size="1.5rem" mb="2rem">{t`Migrate to Cloud`}</Text>

{getStartedVisibleStates.has(migrationState) && (
<MigrationStart
startNewMigration={handleCreateMigration}
isStarting={createCloudMigrationResult.isLoading}
/>
)}

<Box mt="2rem">
{migration && isInProgressMigration(migration) && (
<MigrationInProgress migration={migration} />
)}

{migration && migrationState === "done" && (
<MigrationSuccess
migration={migration}
restartMigration={handleCreateMigration}
isRestarting={createCloudMigrationResult.isLoading}
/>
)}

{migration && migrationState === "error" && (
<MigrationError migration={migration} />
)}
</Box>
</Box>
</LoadingAndErrorWrapper>
);
};

0 comments on commit 8296891

Please sign in to comment.