Skip to content
Snippets Groups Projects
Unverified Commit 20a1235c authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Add model actions section to database admin page sidebar (#27367)

* Add action related stuff to database types

* Add utils checking actions support and state

* Ensure database's `getPlainObject` returns db type

* Add models actions section to database sidebar

* Address feedback
parent a2b7de4c
No related branches found
No related tags found
No related merge requests found
Showing
with 193 additions and 9 deletions
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { NativePermissions } from "metabase-types/api";
import { Database as IDatabase, NativePermissions } from "metabase-types/api";
import { generateSchemaId } from "metabase-lib/metadata/utils/schema";
import { createLookupByProperty, memoizeClass } from "metabase-lib/utils";
import Question from "../Question";
......@@ -31,6 +31,10 @@ class DatabaseInner extends Base {
// Only appears in GET /api/database/:id
"can-manage"?: boolean;
getPlainObject(): IDatabase {
return this._plainObject;
}
// TODO Atte Keinänen 6/11/17: List all fields here (currently only in types/Database)
displayName() {
return this.name;
......
......@@ -7,9 +7,11 @@ export type InitialSyncStatus = "incomplete" | "complete" | "aborted";
export type DatabaseSettings = {
[key: string]: any;
"database-enable-actions"?: boolean;
};
export type DatabaseFeature =
| "actions"
| "basic-aggregations"
| "binning"
| "case-sensitivity-string-filter-options"
......
import { Database, DatabaseData, DatabaseFeature } from "metabase-types/api";
export const COMMON_DATABASE_FEATURES: DatabaseFeature[] = [
"actions",
"basic-aggregations",
"binning",
"case-sensitivity-string-filter-options",
......
import type { Database } from "metabase-types/api";
export const checkDatabaseSupportsActions = (database: Database) =>
database.features.includes("actions");
export const checkDatabaseActionsEnabled = (database: Database) =>
!!database.settings?.["database-enable-actions"];
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
export const ToggleContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
`;
export const Label = styled.label`
width: 100%;
cursor: pointer;
color: ${color("text-medium")};
font-weight: 700;
`;
export const Description = styled.p`
margin-top: 24px;
color: ${color("text-medium")};
line-height: 22px;
`;
import React from "react";
import { t } from "ttag";
import Toggle from "metabase/core/components/Toggle";
import {
ToggleContainer,
Label,
Description,
} from "./ModelActionsSection.styled";
interface ModelActionsSectionProps {
hasModelActionsEnabled: boolean;
onToggleModelActionsEnabled: (enabled: boolean) => void;
}
function ModelActionsSection({
hasModelActionsEnabled,
onToggleModelActionsEnabled,
}: ModelActionsSectionProps) {
return (
<div>
<ToggleContainer>
<Label htmlFor="model-actions-toggle">{t`Model actions`}</Label>
<Toggle
id="model-actions-toggle"
value={hasModelActionsEnabled}
onChange={onToggleModelActionsEnabled}
/>
</ToggleContainer>
<Description>{t`Allow actions from models created from this data to be run. Actions are able to read, write, and possibly delete data.`}</Description>
<Description>{t`Note: Your database user will need write permissions.`}</Description>
</div>
);
}
export default ModelActionsSection;
export { default } from "./ModelActionsSection";
......@@ -46,3 +46,7 @@ export const SidebarContent = styled.div`
margin-bottom: 0;
}
`;
export const ModelActionsSidebarContent = styled(SidebarContent)`
margin-top: 32px;
`;
......@@ -7,19 +7,29 @@ import ConfirmContent from "metabase/components/ConfirmContent";
import ModalWithTrigger from "metabase/components/ModalWithTrigger";
import { isSyncCompleted } from "metabase/lib/syncing";
import {
checkDatabaseSupportsActions,
checkDatabaseActionsEnabled,
} from "metabase/actions/utils";
import DeleteDatabaseModal from "metabase/admin/databases/components/DeleteDatabaseModal.jsx";
import type { DatabaseId } from "metabase-types/api";
import type { Database as IDatabase, DatabaseId } from "metabase-types/api";
import type Database from "metabase-lib/metadata/Database";
import ModelActionsSection from "./ModelActionsSection";
import ModelCachingControl from "./ModelCachingControl";
import { SidebarRoot, SidebarContent, SidebarGroup } from "./Sidebar.styled";
import {
SidebarRoot,
SidebarContent,
SidebarGroup,
ModelActionsSidebarContent,
} from "./Sidebar.styled";
interface DatabaseEditAppSidebarProps {
database: Database;
isAdmin: boolean;
isModelPersistenceEnabled: boolean;
updateDatabase: (database: Database) => void;
updateDatabase: (database: { id: DatabaseId } & Partial<IDatabase>) => void;
syncDatabaseSchema: (databaseId: DatabaseId) => void;
dismissSyncSpinner: (databaseId: DatabaseId) => void;
rescanDatabaseFields: (databaseId: DatabaseId) => void;
......@@ -29,6 +39,7 @@ interface DatabaseEditAppSidebarProps {
const DatabaseEditAppSidebar = ({
database,
updateDatabase,
deleteDatabase,
syncDatabaseSchema,
dismissSyncSpinner,
......@@ -40,33 +51,47 @@ const DatabaseEditAppSidebar = ({
const discardSavedFieldValuesModal = useRef<any>();
const deleteDatabaseModal = useRef<any>();
const isEditingDatabase = !!database.id;
const isSynced = isSyncCompleted(database);
const hasModelActionsSection =
isEditingDatabase &&
checkDatabaseSupportsActions(database.getPlainObject());
const hasModelCachingSection =
isModelPersistenceEnabled && database.supportsPersistence();
const handleSyncDatabaseSchema = useCallback(
() => syncDatabaseSchema(database.id),
[database, syncDatabaseSchema],
[database.id, syncDatabaseSchema],
);
const handleReScanFieldValues = useCallback(
() => rescanDatabaseFields(database.id),
[database, rescanDatabaseFields],
[database.id, rescanDatabaseFields],
);
const handleDismissSyncSpinner = useCallback(
() => dismissSyncSpinner(database.id),
[database, dismissSyncSpinner],
[database.id, dismissSyncSpinner],
);
const handleToggleModelActionsEnabled = useCallback(
(nextValue: boolean) =>
updateDatabase({
id: database.id,
settings: { "database-enable-actions": nextValue },
}),
[database.id, updateDatabase],
);
const handleDiscardSavedFieldValues = useCallback(
() => discardSavedFieldValues(database.id),
[database, discardSavedFieldValues],
[database.id, discardSavedFieldValues],
);
const handleDeleteDatabase = useCallback(
() => deleteDatabase(database.id, true),
[database, deleteDatabase],
[database.id, deleteDatabase],
);
const handleSavedFieldsModalClose = useCallback(() => {
......@@ -162,6 +187,16 @@ const DatabaseEditAppSidebar = ({
</SidebarGroup.List>
</SidebarGroup>
</SidebarContent>
{hasModelActionsSection && (
<ModelActionsSidebarContent>
<ModelActionsSection
hasModelActionsEnabled={checkDatabaseActionsEnabled(
database.getPlainObject(),
)}
onToggleModelActionsEnabled={handleToggleModelActionsEnabled}
/>
</ModelActionsSidebarContent>
)}
</SidebarRoot>
);
};
......
......@@ -163,6 +163,75 @@ describe("DatabaseEditApp/Sidebar", () => {
});
});
describe("model actions control", () => {
it("is shown if database supports actions", () => {
setup();
expect(screen.getByLabelText(/Model actions/i)).toBeInTheDocument();
});
it("is shown for non-admin users", () => {
setup({ isAdmin: false });
expect(screen.getByLabelText(/Model actions/i)).toBeInTheDocument();
});
it("shows if actions are enabled", () => {
setup({
database: createMockDatabase({
settings: { "database-enable-actions": true },
}),
});
expect(screen.getByLabelText(/Model actions/i)).toBeChecked();
});
it("shows if actions are disabled", () => {
setup({
database: createMockDatabase({
settings: { "database-enable-actions": false },
}),
});
expect(screen.getByLabelText(/Model actions/i)).not.toBeChecked();
});
it("isn't shown if database doesn't support actions", () => {
const features = _.without(COMMON_DATABASE_FEATURES, "actions");
setup({ database: createMockDatabase({ features }) });
expect(screen.queryByText(/Model actions/i)).not.toBeInTheDocument();
});
it("isn't shown when adding a new database", () => {
setup({ database: createMockDatabase({ id: undefined }) });
expect(screen.queryByText(/Model actions/i)).not.toBeInTheDocument();
});
it("enables actions", () => {
const { database, updateDatabase } = setup();
userEvent.click(screen.getByLabelText(/Model actions/i));
expect(updateDatabase).toBeCalledWith({
id: database.id,
settings: { "database-enable-actions": true },
});
});
it("disables actions", () => {
const database = createMockDatabase({
settings: { "database-enable-actions": true },
});
const { updateDatabase } = setup({ database });
userEvent.click(screen.getByLabelText(/Model actions/i));
expect(updateDatabase).toBeCalledWith({
id: database.id,
settings: { "database-enable-actions": false },
});
});
});
describe("model caching control", () => {
it("isn't shown if model caching is turned off globally", () => {
setup({ isModelPersistenceEnabled: false });
......
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