Compare commits

...

1 Commits

Author SHA1 Message Date
Raphael Krut-Landau
bdc87e50a1 fix(admin/performance): Convert cache config api to RTK 2024-08-09 10:24:51 -04:00
12 changed files with 188 additions and 42 deletions

View File

@@ -15,7 +15,7 @@ import type { CacheableModel, CacheStrategy } from "metabase-types/api";
import { DashboardStrategySidebarBody } from "./DashboardStrategySidebar.styled";
const configurableModels: CacheableModel[] = ["dashboard"];
const cacheableModels: CacheableModel[] = ["dashboard"];
const DashboardStrategySidebar_Base = ({
dashboard,
@@ -31,7 +31,7 @@ const DashboardStrategySidebar_Base = ({
}
const dashboardId: number = dashboard.id;
const { configs, setConfigs, loading, error } = useCacheConfigs({
configurableModels,
models: cacheableModels,
id: dashboardId,
});

View File

@@ -22,10 +22,10 @@ const SidebarCacheForm_Base = ({
setPage,
...groupProps
}: SidebarCacheFormProps) => {
const configurableModels = useMemo(() => [model], [model]);
const cacheableModels = useMemo(() => [model], [model]);
const id: number = getItemId(model, item);
const { configs, setConfigs, loading, error } = useCacheConfigs({
configurableModels,
models: cacheableModels,
id,
});

View File

@@ -18,10 +18,10 @@ export const SidebarCacheSection = ({
setPage,
}: SidebarCacheSectionProps) => {
const id = useMemo(() => getItemId(model, item), [model, item]);
const configurableModels = useMemo(() => [model], [model]);
const cacheableModels = useMemo(() => [model], [model]);
const { configs, loading, error } = useCacheConfigs({
configurableModels,
models: cacheableModels,
id,
});

View File

@@ -0,0 +1,17 @@
import type { CacheConfig, CacheableModel } from "./performance";
export interface ListCacheConfigsRequest {
model: CacheableModel;
id?: number;
}
export type ListCacheConfigsResponse = {
data: CacheConfig[];
};
export interface GetCacheConfigRequest {
model_id: number;
model: CacheableModel;
}
export type UpdateCacheConfigRequest = CacheConfig;

View File

@@ -1,6 +1,13 @@
/** 'Model' as in 'type of object' */
export type CacheableModel = "root" | "database" | "dashboard" | "question";
export const ALL_CACHEABLE_MODELS: CacheableModel[] = [
"root",
"database",
"dashboard",
"question",
] as const;
export type CacheStrategyType =
| "nocache"
| "ttl" // aka Adaptive

View File

@@ -40,12 +40,12 @@ const StrategyEditorForDatabases_Base = ({
setTargetId,
] = useState<number | null>(null);
const configurableModels: CacheableModel[] = useMemo(() => {
const ret: CacheableModel[] = ["root"];
const cacheableModels: CacheableModel[] = useMemo(() => {
const cacheableModels: CacheableModel[] = ["root"];
if (canOverrideRootStrategy) {
ret.push("database");
cacheableModels.push("database");
}
return ret;
return cacheableModels;
}, [canOverrideRootStrategy]);
const {
@@ -55,7 +55,7 @@ const StrategyEditorForDatabases_Base = ({
rootStrategyRecentlyOverridden,
error: configsError,
loading: areConfigsLoading,
} = useCacheConfigs({ configurableModels });
} = useCacheConfigs({ models: cacheableModels });
const databasesResult = useListDatabasesQuery();
const databases = databasesResult.data?.data ?? [];

View File

@@ -1,5 +1,6 @@
import styled from "@emotion/styled";
import { doNotForwardProps } from "metabase/common/utils/doNotForwardProps";
import { Form } from "metabase/forms";
import type { BoxProps } from "metabase/ui";
import { Box, FixedSizeIcon, Group, Loader } from "metabase/ui";
@@ -26,7 +27,7 @@ export const StyledForm = styled(Form)`
flex-grow: 1;
`;
export const FormBox = styled(Box)<
export const FormBox = styled(Box, doNotForwardProps("isInSidebar"))<
BoxProps & {
isInSidebar?: boolean;
}
@@ -45,7 +46,10 @@ export const FormBox = styled(Box)<
`}
`;
export const StyledFormButtonsGroup = styled(Group)<{ isInSidebar?: boolean }>`
export const StyledFormButtonsGroup = styled(
Group,
doNotForwardProps("isInSidebar"),
)<{ isInSidebar?: boolean }>`
padding-block: 1rem;
gap: 1rem;
background-color: var(--mb-color-bg-white);

View File

@@ -1,40 +1,53 @@
import { useEffect, useState } from "react";
import { useAsync } from "react-use";
import { skipToken } from "@reduxjs/toolkit/query";
import { useEffect, useMemo, useState } from "react";
import _ from "underscore";
import { CacheConfigApi } from "metabase/services";
import type {
CacheConfigAPIResponse,
CacheConfig,
CacheableModel,
} from "metabase-types/api";
import { useListCacheConfigsQuery } from "metabase/api/cache";
import type { CacheConfig, CacheableModel } from "metabase-types/api";
import { rootId } from "../constants/simple";
import { translateConfigFromAPI } from "../utils";
import { useRecentlyTrue } from "./useRecentlyTrue";
const useListCacheConfigsForModel = (
models: CacheableModel[],
model: CacheableModel,
id?: number,
) =>
useListCacheConfigsQuery(
models.includes(model)
? {
model,
id,
}
: skipToken,
);
export const useCacheConfigs = ({
configurableModels,
models,
id,
}: {
configurableModels: CacheableModel[];
models: CacheableModel[];
id?: number;
}) => {
const configsApiResult = useAsync(async () => {
const configsForEachModel = await Promise.all(
configurableModels.map(model =>
CacheConfigApi.list({ model, id }).then(
(response: CacheConfigAPIResponse) => response.data,
),
),
);
const configs = _.flatten(configsForEachModel);
const translatedConfigs = configs.map(translateConfigFromAPI);
return translatedConfigs;
}, [configurableModels, id]);
const rootResult = useListCacheConfigsForModel(models, "root", id);
const databasesResult = useListCacheConfigsForModel(models, "database", id);
const dashboardsResult = useListCacheConfigsForModel(models, "dashboard", id);
const questionsResult = useListCacheConfigsForModel(models, "question", id);
const configsFromAPI = configsApiResult.value;
const { configsFromAPI, isFetching, error } = useMemo(() => {
const results = [
rootResult,
databasesResult,
dashboardsResult,
questionsResult,
];
const configsFromAPI = results.map(result => result.data?.data);
const error = results.find(result => result.error)?.error;
const isFetching = results.some(result => result.isFetching);
return { configsFromAPI, isFetching, error };
}, [rootResult, databasesResult, dashboardsResult, questionsResult]);
const [configs, setConfigs] = useState<CacheConfig[]>([]);
@@ -47,17 +60,17 @@ export const useCacheConfigs = ({
3000,
);
const error = configsApiResult.error;
// The configs are not considered fully loaded until the cache configuration data
// has been loaded from the API _and_ has been copied into local state
const [areConfigsInitialized, setAreConfigsInitialized] =
useState<boolean>(false);
const loading = configsApiResult.loading || !areConfigsInitialized;
const loading = isFetching || !areConfigsInitialized;
useEffect(() => {
if (configsFromAPI) {
setConfigs(configsFromAPI);
const flattenedConfigs = _.compact(configsFromAPI.flat());
const translatedConfigs = flattenedConfigs.map(translateConfigFromAPI);
setConfigs(translatedConfigs);
setAreConfigsInitialized(true);
}
}, [configsFromAPI]);
@@ -67,7 +80,6 @@ export const useCacheConfigs = ({
loading,
configs,
setConfigs,
configsFromAPI,
rootStrategyOverriddenOnce,
rootStrategyRecentlyOverridden,
};

View File

@@ -281,7 +281,11 @@ export const translateConfig = (
config: CacheConfig,
direction: "fromAPI" | "toAPI",
): CacheConfig => {
const translated: CacheConfig = { ...config };
const translated: CacheConfig = {
...config,
// Clone the strategy to make it mutable
strategy: { ...config.strategy },
};
// If strategy type is unsupported, use a fallback
if (!isValidStrategyName(translated.strategy.type)) {

View File

@@ -0,0 +1,78 @@
import type { CacheConfig, CacheableModel } from "metabase-types/api";
import type {
ListCacheConfigsRequest,
ListCacheConfigsResponse,
UpdateCacheConfigRequest,
} from "metabase-types/api/cache-config";
import { Api } from "./api";
import {
cacheIdTag,
invalidateTags,
listTag,
provideCacheConfigListTags,
} from "./tags";
export const cacheConfigApi = Api.injectEndpoints({
endpoints: builder => ({
listCacheConfigs: builder.query<
ListCacheConfigsResponse,
ListCacheConfigsRequest | void
>({
query: params => ({
method: "GET",
url: "/api/cache",
params,
}),
providesTags: response =>
provideCacheConfigListTags(response?.data ?? []),
}),
updateCacheConfig: builder.mutation<CacheConfig, UpdateCacheConfigRequest>({
query: cacheConfig => ({
method: "PUT",
url: "/api/cache",
body: cacheConfig,
}),
invalidatesTags: (_, error, cacheConfig) =>
invalidateTags(error, [
listTag("cache-config"),
cacheIdTag(cacheConfig),
]),
}),
deleteCacheConfig: builder.mutation<
void,
{ model: CacheableModel; model_id: number }
>({
query: cacheConfig => ({
method: "DELETE",
url: "/api/cache",
body: cacheConfig,
}),
invalidatesTags: (_, error, cacheConfig) =>
invalidateTags(error, [
listTag("cache-config"),
cacheIdTag(cacheConfig),
]),
}),
// This mutation invalidates the cache for a specific model (such as a
// question, dashboard, or database). It doesn't need to invalidate RTK's
// cache
invalidateCacheConfig: builder.mutation<
void,
{ model: CacheableModel; model_id: number }
>({
query: cacheConfig => ({
method: "POST",
url: "/api/cache/invalidate",
body: cacheConfig,
}),
}),
}),
});
export const {
useListCacheConfigsQuery,
useUpdateCacheConfigMutation,
useDeleteCacheConfigMutation,
useInvalidateCacheConfigMutation,
} = cacheConfigApi;

View File

@@ -5,6 +5,7 @@ export const TAG_TYPES = [
"alert",
"api-key",
"bookmark",
"cache-config",
"card",
"cloud-migration",
"collection",

View File

@@ -36,6 +36,7 @@ import type {
CardQueryMetadata,
CardId,
ModelIndex,
CacheConfig,
} from "metabase-types/api";
import {
ACTIVITY_MODELS,
@@ -206,6 +207,28 @@ export function provideModelIndexListTags(
];
}
/** Cache configs do not have an id. They are identified by a model (such as
* 'question', 'dashboard', or 'database') and a model_id (the id of the
* question, dashboard, or database) */
export const cacheIdTag = (
cacheConfig: Pick<CacheConfig, "model" | "model_id">,
) => idTag("cache-config", `${cacheConfig.model},${cacheConfig.model_id}`);
export function provideCacheConfigListTags(
cacheConfigs: CacheConfig[],
): TagDescription<TagType>[] {
return [
listTag("cache-config"),
...cacheConfigs.flatMap(provideCacheConfigTags),
];
}
export function provideCacheConfigTags(
cacheConfig: CacheConfig,
): TagDescription<TagType>[] {
return [cacheIdTag(cacheConfig)];
}
export function provideDatabaseCandidateListTags(
candidates: DatabaseXray[],
): TagDescription<TagType>[] {