Skip to content

Commit

Permalink
#9092: slice 2.3 support deleting user deployment in extension console (
Browse files Browse the repository at this point in the history
#9286)

* wip

* wip

* wip

* remove comment

* pr feedback

* pr feedback

* pr feedback

* fix dead code

* pr feedback

* fix types
  • Loading branch information
fungairino authored Oct 16, 2024
1 parent 1cfa77b commit 011fc72
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 49 deletions.
86 changes: 86 additions & 0 deletions src/activation/modOptionsHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ import type { ModDefinition } from "@/types/modDefinitionTypes";
import type { UUID } from "@/types/stringTypes";
import type { OptionsArgs } from "@/types/runtimeTypes";
import type { Schema } from "@/types/schemaTypes";
import type { Deployment, DeploymentPayload } from "@/types/contract";
import { getIsPersonalDeployment } from "@/store/modComponents/modInstanceUtils";
import { PIXIEBRIX_INTEGRATION_ID } from "@/integrations/constants";
import notify from "@/utils/notify";
import {
appApi,
useCreateUserDeploymentMutation,
useDeleteUserDeploymentMutation,
} from "@/data/service/api";
import { useCallback } from "react";
import { type ModInstance } from "@/types/modInstanceTypes";
import type { WizardValues } from "@/activation/wizardTypes";

/**
* Returns the default database name for an auto-created database.
Expand Down Expand Up @@ -79,3 +91,77 @@ export async function autoCreateDatabaseOptionsArgsInPlace(

return optionsArgs;
}

/**
* Handles the logic for creating or deleting a personal deployment for a mod instance
* when activating it. Handles the cases where:
* - The mod instance is already a personal deployment and the user wants to keep it
* (we refetch the deployment and return it since the whole deployment metadata is required for activation)
* - The mod instance is already a personal deployment and the user wants to remove it
* (the deployment is deleted, and we return undefined)
* - The mod instance is not a personal deployment and the user wants to create one
* (a new deployment is created, and we return it)
* - The mod instance is not a personal deployment and the user does not want to create one
* (no action is taken, and we return undefined)
* TODO: Handle activating a mod that is updating to a newer version and update the personal deployment to point to the new mod version
*/
export function useManagePersonalDeployment() {
const [createUserDeployment] = useCreateUserDeploymentMutation();
const [deleteUserDeployment] = useDeleteUserDeploymentMutation();
const [getUserDeployment] = appApi.endpoints.getUserDeployment.useLazyQuery();

return useCallback(
async (
modInstance: ModInstance | undefined,
modDefinition: ModDefinition,
{
personalDeployment: nextIsPersonalDeployment,
integrationDependencies,
optionsArgs,
}: WizardValues,
) => {
let userDeployment: Deployment | undefined;
try {
if (getIsPersonalDeployment(modInstance)) {
if (nextIsPersonalDeployment) {
userDeployment = await getUserDeployment({
id: modInstance.deploymentMetadata.id,
}).unwrap();
} else {
await deleteUserDeployment({
id: modInstance.deploymentMetadata.id,
});
}
} else if (nextIsPersonalDeployment) {
const data: DeploymentPayload = {
name: `Personal deployment for ${modDefinition.metadata.name}, version ${modDefinition.metadata.version}`,
services: integrationDependencies
.map((integrationDependency) =>
integrationDependency.integrationId !==
PIXIEBRIX_INTEGRATION_ID &&
integrationDependency.configId != null
? {
auth: integrationDependency.configId,
}
: null,
)
.filter((x) => x != null),
options_config: optionsArgs,
};
userDeployment = await createUserDeployment({
modDefinition,
data,
}).unwrap();
}
} catch (error) {
notify.error({
message: `Error setting up device synchronization for ${modDefinition.metadata.name}. Please try reactivating.`,
error,
});
}

return userDeployment;
},
[createUserDeployment, deleteUserDeployment, getUserDeployment],
);
}
113 changes: 112 additions & 1 deletion src/activation/useActivateMod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
type Deployment,
type EditablePackageMetadata,
} from "@/types/contract";
import { activatableDeploymentFactory } from "@/testUtils/factories/deploymentFactories";

jest.mock("@/contentScript/messenger/api");
jest.mock("@/utils/notify");
Expand Down Expand Up @@ -343,7 +344,7 @@ describe("useActivateMod", () => {
});

describe("personal deployment functionality", () => {
const packageVersionId = "package-version-id";
const packageVersionId = uuidv4();
const testDeployment = {
id: uuidv4(),
name: "test-user-deployment",
Expand Down Expand Up @@ -529,5 +530,115 @@ describe("useActivateMod", () => {
}),
});
});

it("handles personal deployment deletion successfully", async () => {
appApiMock
.onDelete(API_PATHS.USER_DEPLOYMENT(testDeployment.id))
.reply(204);

const { result, getReduxStore } = renderHook(
() => useActivateMod("marketplace"),
{
setupRedux(dispatch, { store }) {
dispatch(
modComponentSlice.actions.activateMod({
modDefinition,
screen: "extensionConsole",
deployment: activatableDeploymentFactory({
modDefinitionOverride: modDefinition,
deploymentOverride: {
name: testDeployment.name,
id: testDeployment.id,
},
}).deployment,
isReactivate: false,
}),
);
jest.spyOn(store, "dispatch");
},
},
);

const { success, error } = await result.current(
{ ...formValues, personalDeployment: false },
modDefinition,
);

expect(success).toBe(true);
expect(error).toBeUndefined();

const { dispatch } = getReduxStore();

expect(dispatch).toHaveBeenCalledWith(
modComponentSlice.actions.activateMod({
modDefinition,
deployment: undefined,
configuredDependencies: [],
optionsArgs: formValues.optionsArgs,
screen: "marketplace",
isReactivate: true,
}),
);

expect(appApiMock.history.delete).toHaveLength(1);
expect(appApiMock.history.delete[0]!.url).toBe(
API_PATHS.USER_DEPLOYMENT(testDeployment.id),
);
});

it("handles reactivating with personal deployment already active", async () => {
appApiMock
.onGet(API_PATHS.USER_DEPLOYMENT(testDeployment.id))
.reply(200, testDeployment);

const { result, getReduxStore } = renderHook(
() => useActivateMod("marketplace"),
{
setupRedux(dispatch, { store }) {
dispatch(
modComponentSlice.actions.activateMod({
modDefinition,
screen: "extensionConsole",
deployment: activatableDeploymentFactory({
modDefinitionOverride: modDefinition,
deploymentOverride: {
name: testDeployment.name,
id: testDeployment.id,
},
}).deployment,
isReactivate: false,
}),
);
jest.spyOn(store, "dispatch");
},
},
);

const { success, error } = await result.current(
{ ...formValues, personalDeployment: true },
modDefinition,
);

expect(success).toBe(true);
expect(error).toBeUndefined();

const { dispatch } = getReduxStore();

expect(dispatch).toHaveBeenCalledWith(
modComponentSlice.actions.activateMod({
modDefinition,
deployment: testDeployment,
configuredDependencies: [],
optionsArgs: formValues.optionsArgs,
screen: "marketplace",
isReactivate: true,
}),
);

expect(appApiMock.history.get).toHaveLength(1);
expect(appApiMock.history.get[0]!.url).toBe(
API_PATHS.USER_DEPLOYMENT(testDeployment.id),
);
});
});
});
60 changes: 13 additions & 47 deletions src/activation/useActivateMod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,15 @@ import { getErrorMessage } from "@/errors/errorHelpers";
import { deactivateMod } from "@/store/deactivateUtils";
import { ensurePermissionsFromUserGesture } from "@/permissions/permissionsUtils";
import { checkModDefinitionPermissions } from "@/modDefinitions/modDefinitionPermissionsHelpers";
import {
useCreateDatabaseMutation,
useCreateUserDeploymentMutation,
} from "@/data/service/api";
import { useCreateDatabaseMutation } from "@/data/service/api";
import { Events } from "@/telemetry/events";
import { reloadModsEveryTab } from "@/contentScript/messenger/api";
import { autoCreateDatabaseOptionsArgsInPlace } from "@/activation/modOptionsHelpers";
import {
autoCreateDatabaseOptionsArgsInPlace,
useManagePersonalDeployment,
} from "@/activation/modOptionsHelpers";
import { type ReportEventData } from "@/telemetry/telemetryTypes";
import { type Deployment, type DeploymentPayload } from "@/types/contract";
import { PIXIEBRIX_INTEGRATION_ID } from "@/integrations/constants";
import notify from "@/utils/notify";
import { selectModInstanceMap } from "@/store/modComponents/modInstanceSelectors";
import { getIsPersonalDeployment } from "@/store/modComponents/modInstanceUtils";

export type ActivateResult = {
success: boolean;
Expand Down Expand Up @@ -85,7 +81,7 @@ function useActivateMod(
const modInstanceMap = useSelector(selectModInstanceMap);

const [createDatabase] = useCreateDatabaseMutation();
const [createUserDeployment] = useCreateUserDeploymentMutation();
const handleUserDeployment = useManagePersonalDeployment();

return useCallback(
async (formValues: WizardValues, modDefinition: ModDefinition) => {
Expand Down Expand Up @@ -155,41 +151,11 @@ function useActivateMod(
dispatch,
);

// TODO: handle updating a deployment from a previous version to the new version and
// handle deleting a deployment if the user turns off personal deployment
// https://github.com/pixiebrix/pixiebrix-extension/issues/9092
let createdUserDeployment: Deployment | undefined;
if (
formValues.personalDeployment &&
// Avoid creating a personal deployment if the mod is already associated with one
!getIsPersonalDeployment(modInstance)
) {
const data: DeploymentPayload = {
name: `Personal deployment for ${modDefinition.metadata.name}, version ${modDefinition.metadata.version}`,
services: integrationDependencies.flatMap(
(integrationDependency) =>
integrationDependency.integrationId ===
PIXIEBRIX_INTEGRATION_ID ||
integrationDependency.configId == null
? []
: [{ auth: integrationDependency.configId }],
),
options_config: optionsArgs,
};
const result = await createUserDeployment({
modDefinition,
data,
});

if ("error" in result) {
notify.error({
message: `Error setting up device synchronization for ${modDefinition.metadata.name}. Please try reactivating.`,
error: result.error,
});
} else {
createdUserDeployment = result.data;
}
}
const userDeployment = await handleUserDeployment(
modInstance,
modDefinition,
formValues,
);

dispatch(
modComponentSlice.actions.activateMod({
Expand All @@ -198,7 +164,7 @@ function useActivateMod(
optionsArgs,
screen: source,
isReactivate,
deployment: createdUserDeployment,
deployment: userDeployment,
}),
);

Expand Down Expand Up @@ -226,7 +192,7 @@ function useActivateMod(
checkPermissions,
dispatch,
createDatabase,
createUserDeployment,
handleUserDeployment,
],
);
}
Expand Down
12 changes: 12 additions & 0 deletions src/data/service/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,17 @@ export const appApi = createApi({
},
invalidatesTags: ["Deployments"],
}),
getUserDeployment: builder.query<Deployment, { id: UUID }>({
query({ id }) {
return { url: API_PATHS.USER_DEPLOYMENT(id), method: "get" };
},
}),
deleteUserDeployment: builder.mutation<void, { id: UUID }>({
query({ id }) {
return { url: API_PATHS.USER_DEPLOYMENT(id), method: "delete" };
},
invalidatesTags: ["Deployments"],
}),
}),
});

Expand Down Expand Up @@ -518,5 +529,6 @@ export const {
useCreateMilestoneMutation,
useGetDeploymentsQuery,
useCreateUserDeploymentMutation,
useDeleteUserDeploymentMutation,
util,
} = appApi;
2 changes: 2 additions & 0 deletions src/data/service/urlPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export const API_PATHS = {
`/api/deployments/${deploymentId}/alerts/`,

USER_DEPLOYMENTS: "/api/me/deployments/",
USER_DEPLOYMENT: (deploymentId: string) =>
`/api/me/deployments/${deploymentId}/`,

FEATURE_FLAGS: "/api/me/",

Expand Down
5 changes: 4 additions & 1 deletion src/store/modComponents/modInstanceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ export function generateModInstanceId(): ModInstanceId {
*/
export function getIsPersonalDeployment(
modInstance: ModInstance | undefined,
): boolean {
): modInstance is ModInstance & {
deploymentMetadata: DeploymentMetadata;
isPersonalDeployment: true;
} {
return Boolean(modInstance?.deploymentMetadata?.isPersonalDeployment);
}

Expand Down

0 comments on commit 011fc72

Please sign in to comment.