Skip to content
Snippets Groups Projects
Unverified Commit 0f551593 authored by github-automation-metabase's avatar github-automation-metabase Committed by GitHub
Browse files

[Clean up] Hide clean up UI in sample collections (#50760) (#50810)


* hides clean up ui on sample collections

* commit unsaved changes

* pr feedback

Co-authored-by: default avatarSloan Sparger <sloansparger@users.noreply.github.com>
parent 22373dcd
No related branches found
Tags v0.52.0.5-beta v1.52.0.5-beta
No related merge requests found
import dayjs from "dayjs";
import { assocIn } from "icepick";
import { P, isMatching } from "ts-pattern";
import { SAMPLE_DB_TABLES } from "e2e/support/cypress_data";
......@@ -48,322 +49,356 @@ describe("scenarios > collections > clean up", () => {
});
});
describeEE("action menu", () => {
it("should show in proper contexts", () => {
cy.signInAsAdmin();
setTokenFeatures("all");
cy.log("should not show in custom analytics collections");
visitCollection("root");
navigationSidebar().within(() => {
cy.findByText("Usage analytics").click();
cy.findByText("Custom reports").click();
});
collectionMenu().click();
popover().within(() => {
cy.findByText("Clean things up").should("not.exist");
});
cy.log(
"should show in a normal collection that user has write access to",
);
visitCollection(FIRST_COLLECTION_ID);
collectionMenu().click();
popover().within(() => {
cy.findByText("Clean things up").should("exist");
});
cy.log("should not show in custom analytics collections");
popover().within(() => {
cy.findByText("Move to trash").click();
});
modal().within(() => {
cy.findByText("Move to trash").click();
});
cy.findByTestId("archive-banner").should("exist");
getCollectionActions().should("not.exist");
cy.log("empty collection");
createCollection({ name: "Empty" }).then(({ body: { id } }) => {
visitCollection(id);
describeEE("ee", () => {
describeEE("action menu", () => {
it("should show in proper contexts", () => {
cy.signInAsAdmin();
setTokenFeatures("all");
cy.log("should not show in custom analytics collections");
visitCollection("root");
navigationSidebar().within(() => {
cy.findByText("Usage analytics").click();
cy.findByText("Custom reports").click();
});
collectionMenu().click();
popover().within(() => {
cy.findByText("Clean things up").should("not.exist");
});
});
});
it("should not show to users who do not have write permissions to a collection", () => {
cy.signIn("readonly");
visitCollection(FIRST_COLLECTION_ID);
collectionMenu().should("not.exist");
});
});
cy.log(
"should show in a normal collection that user has write access to",
);
visitCollection(FIRST_COLLECTION_ID);
collectionMenu().click();
popover().within(() => {
cy.findByText("Clean things up").should("exist");
});
describeWithSnowplowEE("clean up collection modal", () => {
beforeEach(() => {
resetSnowplow();
cy.signInAsAdmin();
setTokenFeatures("all");
enableTracking();
});
cy.log("should not show in custom analytics collections");
popover().within(() => {
cy.findByText("Move to trash").click();
});
modal().within(() => {
cy.findByText("Move to trash").click();
});
cy.findByTestId("archive-banner").should("exist");
getCollectionActions().should("not.exist");
afterEach(() => {
expectNoBadSnowplowEvents();
});
cy.log("should not show in empty collections");
createCollection({ name: "Empty" }).then(({ body: { id } }) => {
visitCollection(id);
collectionMenu().click();
popover().within(() => {
cy.findByText("Clean things up").should("not.exist");
});
});
it("should be able to clean up stale items", () => {
seedMainTestData().then(seedData => {
const firstAlphabeticalName = "Bulk dashboard 1";
const lastAlphabeticalName = "Bulk question 9";
cy.log("should not show in sample collections");
createCollection({ name: "Fake sample collection" }).then(
({ body: { id } }) => {
// make sure that it has one stale item
bulkCreateQuestions(1, { collection_id: id }).then(([question]) => {
makeItemsStale(
[question.id],
"card",
dayjs()
.startOf("day")
.subtract(6, "months")
.format("YYYY-MM-DD"),
);
// trip is_sample flag
cy.intercept("GET", `/api/collection/${id}`, req => {
req.on("response", res => {
res.send(assocIn(res.body, ["is_sample"], true));
});
});
// assert we don't show clean up option
visitCollection(id);
collectionMenu().click();
popover().within(() => {
cy.findByText("Clean things up").should("not.exist");
});
cleanUpAlert().should("not.exist");
});
},
);
});
cy.log("should be able to navigate to clean up modal");
visitCollection(seedData.collection.id);
selectCleanThingsUpCollectionAction();
cy.url().should("include", "cleanup");
it("should not show to users who do not have write permissions to a collection", () => {
cy.signIn("readonly");
visitCollection(FIRST_COLLECTION_ID);
collectionMenu().should("not.exist");
});
});
cy.log("should render all items of current collection");
assertStaleItemCount(seedData.totalStaleItemCount);
describeWithSnowplowEE("clean up collection modal", () => {
beforeEach(() => {
resetSnowplow();
cy.signInAsAdmin();
setTokenFeatures("all");
enableTracking();
});
cy.log("should be able to filter to fewer items");
setDateFilter("1 year");
assertNoPagination();
afterEach(() => {
expectNoBadSnowplowEvents();
});
cy.log("should be able to recursively show stale items");
setDateFilter("6 months");
recursiveFilter().click();
assertStaleItemCount(seedData.recursiveTotalItemCount);
it("should be able to clean up stale items", () => {
seedMainTestData().then(seedData => {
const firstAlphabeticalName = "Bulk dashboard 1";
const lastAlphabeticalName = "Bulk question 9";
cy.log("pagination should work as expected");
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
});
cleanUpModal().within(() => {
cy.findByText(lastAlphabeticalName).should("not.exist");
});
pagination().within(() => {
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
cleanUpModal().within(() => {
cy.findByText(lastAlphabeticalName).should("exist");
});
pagination().within(() => {
cy.findAllByTestId("previous-page-btn").click();
cy.findByText("1 - 10").should("exist");
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
cy.log("should be able to navigate to clean up modal");
visitCollection(seedData.collection.id);
selectCleanThingsUpCollectionAction();
cy.url().should("include", "cleanup");
cy.log("pagination should reset when the date filter changes");
setDateFilter("3 months");
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
cy.log("should render all items of current collection");
assertStaleItemCount(seedData.totalStaleItemCount);
// pagination should reset when the recursive filter changes
recursiveFilter().click();
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
});
recursiveFilter().click();
cy.log("should be able to filter to fewer items");
setDateFilter("1 year");
assertNoPagination();
cy.log("should be able to sort items by name and last used at columns");
cleanUpModal().within(() => {
cy.get("table").within(() => {
cy.findByText(firstAlphabeticalName).should("exist");
cy.log("should be able to recursively show stale items");
setDateFilter("6 months");
recursiveFilter().click();
assertStaleItemCount(seedData.recursiveTotalItemCount);
cy.log("pagination should work as expected");
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
});
cleanUpModal().within(() => {
cy.findByText(lastAlphabeticalName).should("not.exist");
cy.findByText(/Name/).click();
cy.findByText(firstAlphabeticalName).should("not.exist");
});
pagination().within(() => {
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
cleanUpModal().within(() => {
cy.findByText(lastAlphabeticalName).should("exist");
cy.findByText(/Name/).click();
cy.findByText(firstAlphabeticalName).should("exist");
cy.findByText(lastAlphabeticalName).should("not.exist");
});
});
pagination().within(() => {
cy.findAllByTestId("previous-page-btn").click();
cy.findByText("1 - 10").should("exist");
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
cy.log("should be able to move stale items to the trash");
recursiveFilter().click();
assertStaleItemCount(seedData.totalStaleItemCount);
selectAllItems();
moveToTrash();
assertNoPagination();
expectGoodSnowplowEvent(
event =>
isMatching(
{
event: "moved-to-trash",
event_detail: P.union("dashboard", "question"),
target_id: P.number,
triggered_from: "cleanup_modal",
duration_ms: P.number,
result: "success",
},
event,
),
10,
);
cy.log("pagination should reset when the date filter changes");
setDateFilter("3 months");
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
cy.findAllByTestId("next-page-btn").click();
cy.findByText("11 - 19").should("exist");
});
// Because cutoff_date will be relative to the current date, we simply check
// that it exists and is a string. Snowplow will assert that it is in the correct
// format
expectGoodSnowplowEvent(
event =>
event &&
event.event === "stale_items_archived" &&
event.collection_id === seedData.collection.id &&
event.total_items_archived === 10 &&
typeof event.cutoff_date === "string",
);
// pagination should reset when the recursive filter changes
recursiveFilter().click();
pagination().within(() => {
cy.findByText("1 - 10").should("exist");
});
recursiveFilter().click();
undo();
assertStaleItemCount(seedData.totalStaleItemCount);
cy.log(
"should be able to sort items by name and last used at columns",
);
cleanUpModal().within(() => {
cy.get("table").within(() => {
cy.findByText(firstAlphabeticalName).should("exist");
cy.findByText(lastAlphabeticalName).should("not.exist");
cy.findByText(/Name/).click();
cy.findByText(firstAlphabeticalName).should("not.exist");
cy.findByText(lastAlphabeticalName).should("exist");
cy.findByText(/Name/).click();
cy.findByText(firstAlphabeticalName).should("exist");
cy.findByText(lastAlphabeticalName).should("not.exist");
});
});
selectAllItems();
moveToTrash();
assertNoPagination();
cy.log("should be able to move stale items to the trash");
recursiveFilter().click();
assertStaleItemCount(seedData.totalStaleItemCount);
selectAllItems();
moveToTrash();
assertNoPagination();
expectGoodSnowplowEvent(
event =>
isMatching(
{
event: "moved-to-trash",
event_detail: P.union("dashboard", "question"),
target_id: P.number,
triggered_from: "cleanup_modal",
duration_ms: P.number,
result: "success",
},
event,
),
10,
);
selectAllItems();
moveToTrash();
// Because cutoff_date will be relative to the current date, we simply check
// that it exists and is a string. Snowplow will assert that it is in the correct
// format
expectGoodSnowplowEvent(
event =>
event &&
event.event === "stale_items_archived" &&
event.collection_id === seedData.collection.id &&
event.total_items_archived === 10 &&
typeof event.cutoff_date === "string",
);
closeCleanUpModal();
cy.url().should("not.include", "cleanup");
undo();
assertStaleItemCount(seedData.totalStaleItemCount);
cy.log(
"collection items view should reflect the actions taken in the clean up modal",
);
main().within(() => {
cy.get("tr").should(
"have.length",
seedData.notStaleItemCount +
1 + // child collection
1, // header row
selectAllItems();
moveToTrash();
assertNoPagination();
selectAllItems();
moveToTrash();
closeCleanUpModal();
cy.url().should("not.include", "cleanup");
cy.log(
"collection items view should reflect the actions taken in the clean up modal",
);
});
main().within(() => {
cy.get("tr").should(
"have.length",
seedData.notStaleItemCount +
1 + // child collection
1, // header row
);
});
makeItemStale(ORDERS_QUESTION_ID, "card");
makeItemStale(ORDERS_QUESTION_ID, "card");
cy.findByRole("navigation").findByText("Our analytics").click();
selectCleanThingsUpCollectionAction();
cy.url().should("include", "cleanup");
selectAllItems();
moveToTrash();
// Ensure that stale items in Our Analytics are maked with a null collection id
expectGoodSnowplowEvent(
event =>
event &&
event.event === "stale_items_archived" &&
event.collection_id === null &&
event.total_items_archived === 1 &&
typeof event.cutoff_date === "string",
);
cy.findByRole("navigation").findByText("Our analytics").click();
selectCleanThingsUpCollectionAction();
cy.url().should("include", "cleanup");
selectAllItems();
moveToTrash();
// Ensure that stale items in Our Analytics are maked with a null collection id
expectGoodSnowplowEvent(
event =>
event &&
event.event === "stale_items_archived" &&
event.collection_id === null &&
event.total_items_archived === 1 &&
typeof event.cutoff_date === "string",
);
});
});
});
it("show empty and error states correctly", () => {
cy.log("should handle empty state");
cy.intercept("GET", "/api/ee/stale/**?**").as("stale-items");
it("show empty and error states correctly", () => {
cy.log("should handle empty state");
cy.intercept("GET", "/api/ee/stale/**?**").as("stale-items");
// visit collection w/ items but no stale items
createCollection({ name: "Not empty w/ not stale items" })
.then(({ body: { id } }) => id)
.as("collectionId");
// visit collection w/ items but no stale items
createCollection({ name: "Not empty w/ not stale items" })
.then(({ body: { id } }) => id)
.as("collectionId");
cy.get("@collectionId").then(id => {
return bulkCreateQuestions(2, { collection_id: id }).then(() => {
visitCollection(id);
cy.get("@collectionId").then(id => {
return bulkCreateQuestions(2, { collection_id: id }).then(() => {
visitCollection(id);
});
});
});
cy.log("should render a table w/ contents");
main().within(() => {
cy.findByText("Type");
cy.findByText("Name");
});
cy.log("should render a table w/ contents");
main().within(() => {
cy.findByText("Type");
cy.findByText("Name");
});
selectCleanThingsUpCollectionAction();
selectCleanThingsUpCollectionAction();
cy.wait("@stale-items");
cy.wait("@stale-items");
cleanUpModal().within(() => {
emptyState().should("exist");
});
cleanUpModal().within(() => {
emptyState().should("exist");
});
cy.log("should handle error state");
cy.intercept("GET", "/api/ee/stale/**?**", {
statusCode: 500,
}).as("stale-items");
cy.log("should handle error state");
cy.intercept("GET", "/api/ee/stale/**?**", {
statusCode: 500,
}).as("stale-items");
setDateFilter("1 year");
cy.wait("@stale-items");
errorState().should("exist");
setDateFilter("1 year");
cy.wait("@stale-items");
errorState().should("exist");
});
});
});
describeEE("clean up collection alert", () => {
beforeEach(() => {
cy.signInAsAdmin();
setTokenFeatures("all");
});
describeEE("clean up collection alert", () => {
beforeEach(() => {
cy.signInAsAdmin();
setTokenFeatures("all");
});
it("should show admins clean up alert if there's something to clean up in a collection", () => {
cy.log("should not show alert if there's nothing stale");
cy.intercept("GET", "/api/ee/stale/*").as("staleItems");
visitCollection(FIRST_COLLECTION_ID);
cy.wait("@staleItems");
cleanUpAlert().should("not.exist");
it("should show admins clean up alert if there's something to clean up in a collection", () => {
cy.log("should not show alert if there's nothing stale");
cy.intercept("GET", "/api/ee/stale/*").as("staleItems");
visitCollection(FIRST_COLLECTION_ID);
// seed slightly stale content
cy.createQuestion({
name: "Not stale enough",
collection_id: FIRST_COLLECTION_ID,
query: { "source-table": STATIC_ORDERS_ID },
}).then(req => {
makeItemsStale(
[req.body.id],
"card",
dayjs().startOf("day").subtract(2, "months").format("YYYY-MM-DD"),
);
// seed slightly stale content
cy.createQuestion({
name: "Not stale enough",
collection_id: FIRST_COLLECTION_ID,
query: { "source-table": STATIC_ORDERS_ID },
}).then(req => {
makeItemsStale(
[req.body.id],
"card",
dayjs().startOf("day").subtract(2, "months").format("YYYY-MM-DD"),
);
cy.log("should not be shown with 2 month stale content");
cy.reload();
cleanUpAlert().should("not.exist");
});
cy.log("should not be shown with 2 month stale content");
cy.reload();
cleanUpAlert().should("not.exist");
});
// seed stale enough content
cy.createQuestion({
name: "Stale enough",
collection_id: FIRST_COLLECTION_ID,
query: { "source-table": STATIC_ORDERS_ID },
}).then(req => {
makeItemsStale(
[req.body.id],
"card",
dayjs().startOf("day").subtract(3, "months").format("YYYY-MM-DD"),
);
// seed stale enough content
cy.createQuestion({
name: "Stale enough",
collection_id: FIRST_COLLECTION_ID,
query: { "source-table": STATIC_ORDERS_ID },
}).then(req => {
makeItemsStale(
[req.body.id],
"card",
dayjs().startOf("day").subtract(3, "months").format("YYYY-MM-DD"),
);
cy.log("should be shown with 3 month stale content");
cy.reload();
cleanUpAlert()
.should("exist")
.findByText(/Get rid of unused content/)
.click();
cy.url().should("include", "cleanup");
closeCleanUpModal();
});
cy.log("should be shown with 3 month stale content");
cy.log("should not show alert if user is not admin");
cy.signOut();
cy.signInAsNormalUser();
cy.reload();
cleanUpAlert()
.should("exist")
.findByText(/Get rid of unused content/)
.click();
cy.url().should("include", "cleanup");
closeCleanUpModal();
cy.wait("@staleItems");
cleanUpAlert().should("not.exist");
});
cy.log("should not show alert if user is not admin");
cy.signOut();
cy.signInAsNormalUser();
cy.reload();
cy.wait("@staleItems");
cleanUpAlert().should("not.exist");
});
});
});
......
......@@ -5,6 +5,7 @@ import Link from "metabase/core/components/Link";
import { color } from "metabase/lib/colors";
import { useSelector } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { PLUGIN_COLLECTIONS } from "metabase/plugins";
import { getUserIsAdmin } from "metabase/selectors/user";
import { Alert, Box, Icon } from "metabase/ui";
import { useListStaleCollectionItemsQuery } from "metabase-enterprise/api/collection";
......@@ -27,13 +28,15 @@ export const CollectionCleanupAlert = ({
collection: Collection;
}) => {
const isAdmin = useSelector(getUserIsAdmin);
const shouldFetchStaleItems =
isAdmin && PLUGIN_COLLECTIONS.canCleanUp(collection);
const {
data: staleItems,
isLoading,
error,
} = useListStaleCollectionItemsQuery(
isAdmin
shouldFetchStaleItems
? {
id: collection.id,
limit: 0, // only fetch pagination info
......
import { t } from "ttag";
import { ModalRoute } from "metabase/hoc/ModalRoute";
import * as Urls from "metabase/lib/urls";
import { PLUGIN_COLLECTIONS } from "metabase/plugins";
import { hasPremiumFeature } from "metabase-enterprise/settings";
import { CleanupCollectionModal } from "./CleanupCollectionModal";
import { CollectionCleanupAlert } from "./CollectionCleanupAlert";
import { canCleanUp } from "./utils";
if (hasPremiumFeature("collection_cleanup")) {
PLUGIN_COLLECTIONS.canCleanUp = true;
PLUGIN_COLLECTIONS.canCleanUp = canCleanUp;
PLUGIN_COLLECTIONS.getCleanUpMenuItems = (
itemCount,
url,
isInstanceAnalyticsCustom,
isTrashed,
canWrite,
) => {
const canCleanUpCollection =
itemCount !== 0 && !isInstanceAnalyticsCustom && !isTrashed && canWrite;
if (!canCleanUpCollection) {
PLUGIN_COLLECTIONS.getCleanUpMenuItems = (collection, itemCount) => {
if (!canCleanUp(collection) || itemCount === 0) {
return [];
}
......@@ -28,7 +21,7 @@ if (hasPremiumFeature("collection_cleanup")) {
{
title: t`Clean things up`,
icon: "archive",
link: `${url}/cleanup`,
link: `${Urls.collection(collection)}/cleanup`,
},
];
};
......
import {
isInstanceAnalyticsCustomCollection,
isTrashedCollection,
} from "metabase/collections/utils";
import type { Collection } from "metabase-types/api";
export function canCleanUp(collection: Collection): boolean {
return Boolean(
!isInstanceAnalyticsCustomCollection(collection) &&
!isTrashedCollection(collection) &&
!collection.is_sample &&
collection.can_write,
);
}
import { PLUGIN_COLLECTIONS } from "metabase/plugins";
import { createMockCollection } from "metabase-types/api/mocks";
import { canCleanUp } from "./utils";
describe("canCleanUp", () => {
it("does not allow cleaning up analytics collection", () => {
const collection = createMockCollection({
entity_id:
PLUGIN_COLLECTIONS.CUSTOM_INSTANCE_ANALYTICS_COLLECTION_ENTITY_ID,
});
expect(canCleanUp(collection)).toBe(false);
});
it("does not allow cleaning up the root trash collection", () => {
const collection = createMockCollection({ type: "trash" });
expect(canCleanUp(collection)).toBe(false);
});
it("does not allow cleaning up a collection in the trash", () => {
const collection = createMockCollection({ archived: true });
expect(canCleanUp(collection)).toBe(false);
});
it("does not allow cleaning up a sample collection", () => {
const collection = createMockCollection({ is_sample: true });
expect(canCleanUp(collection)).toBe(false);
});
it("does not allow cleaning up a collection when the user does not have write access", () => {
const collection = createMockCollection({ can_write: true });
expect(canCleanUp(collection)).toBe(true);
});
});
......@@ -69,6 +69,7 @@ export interface Collection {
parent_id?: CollectionId | null;
personal_owner_id?: UserId;
is_personal?: boolean;
is_sample?: boolean; // true if the collection part of the sample content
location: string | null;
effective_location?: string; // location path containing only those collections that the user has permission to access
......
......@@ -5,7 +5,6 @@ import {
isInstanceAnalyticsCustomCollection,
isRootCollection,
isRootPersonalCollection,
isTrashedCollection,
} from "metabase/collections/utils";
import EntityMenu from "metabase/components/EntityMenu";
import * as Urls from "metabase/lib/urls";
......@@ -33,7 +32,7 @@ export const CollectionMenu = ({
limit: 0, // we don't want any of the items, we just want to know how many there are in the collection
},
{
skip: !PLUGIN_COLLECTIONS.canCleanUp,
skip: !PLUGIN_COLLECTIONS.canCleanUp(collection),
},
).data?.total ?? 0;
......@@ -43,7 +42,6 @@ export const CollectionMenu = ({
const isPersonal = isRootPersonalCollection(collection);
const isInstanceAnalyticsCustom =
isInstanceAnalyticsCustomCollection(collection);
const isTrashed = isTrashedCollection(collection);
const canWrite = collection.can_write;
const canMove =
......@@ -76,11 +74,8 @@ export const CollectionMenu = ({
items.push(
...PLUGIN_COLLECTIONS.getCleanUpMenuItems(
collection,
maybeCollectionItemCount,
url,
isInstanceAnalyticsCustom,
isTrashed,
canWrite,
),
);
......
......@@ -324,13 +324,10 @@ export const PLUGIN_COLLECTIONS = {
filterOutItemsFromInstanceAnalytics: <Item extends ItemWithCollection>(
items: Item[],
) => items as Item[],
canCleanUp: false,
canCleanUp: (_collection: Collection) => false as boolean,
getCleanUpMenuItems: (
_collection: Collection,
_itemCount: number,
_url: string,
_isInstanceAnalyticsCustom: boolean,
_isTrashed: boolean,
_canWrite: boolean,
): CleanUpMenuItem[] => [],
cleanUpRoute: null as React.ReactElement | null,
cleanUpAlert: (() => null) as (props: {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment