Skip to content
Snippets Groups Projects
Unverified Commit 2ab84940 authored by Nemanja Glumac's avatar Nemanja Glumac Committed by GitHub
Browse files

Introduce `task` api using RTK Query and use it to replace `Tasks` entity (#41452)

* Throw for unused Tasks endpoints

* RTK Query `taskApi`

* Wire tasks entity list and get endpoints to use RTK Query

* Fix typo

* Expose `taskApi`

* Rework `TaskModal` to use RTK Query api

* Throw on `Tasks.api.get`

* Rework `TaskApp` to use RTK Query api

* Delete `Tasks` entity

... which never should've been an entity in the first place.

* Delete unused `TaskApi` entry

* Fix next pagination page not being properly disabled

* Do not export `isLastPage` function

* Convert TasksApp to TS

* Use Mantine Tooltip

* Remove stray comment

* Add empty lines for better legibility

* Update the response type

* Define `task_details` type more strictly

* Use the existing `DatabaseId` type

* Provide tags for `list` and `get` endpoints

* Address code review

* Handle error and loading in the `TasksApp`

* Handle error and loading in the `TaskModal`

* Extract the page size into a variable

* Index databases without mutation

* Refactor TaskModal
parent 37067101
No related branches found
No related tags found
No related merge requests found
Showing
with 234 additions and 165 deletions
......@@ -31,6 +31,7 @@ export * from "./slack";
export * from "./snippets";
export * from "./store";
export * from "./table";
export * from "./task";
export * from "./timeline";
export * from "./user";
export * from "./util";
......
import type { DatabaseId } from "./database";
import type { PaginationRequest, PaginationResponse } from "./pagination";
export interface Task {
id: number;
db_id: DatabaseId | null;
duration: number;
started_at: string;
ended_at: string;
task: string;
task_details: Record<string, unknown> | null;
}
export type ListTasksRequest = PaginationRequest;
export type ListTasksResponse = { data: Task[] } & PaginationResponse;
......@@ -34,8 +34,8 @@ import {
ModelCacheRefreshJobs,
ModelCacheRefreshJobModal,
} from "metabase/admin/tasks/containers/ModelCacheRefreshJobs";
import TaskModal from "metabase/admin/tasks/containers/TaskModal";
import TasksApp from "metabase/admin/tasks/containers/TasksApp";
import { TaskModal } from "metabase/admin/tasks/containers/TaskModal";
import { TasksApp } from "metabase/admin/tasks/containers/TasksApp";
import TroubleshootingApp from "metabase/admin/tasks/containers/TroubleshootingApp";
import Tools from "metabase/admin/tools/containers/Tools";
import {
......
/* eslint-disable react/prop-types */
import { Component } from "react";
import { connect } from "react-redux";
import { goBack } from "react-router-redux";
import { t } from "ttag";
import _ from "underscore";
import Code from "metabase/components/Code";
import ModalContent from "metabase/components/ModalContent";
import Task from "metabase/entities/tasks";
class TaskModalInner extends Component {
render() {
const { object } = this.props;
return (
<ModalContent title={t`Task details`} onClose={() => this.props.goBack()}>
<Code>{JSON.stringify(object.task_details)}</Code>
</ModalContent>
);
}
}
const TaskModal = _.compose(
Task.load({
id: (state, props) => props.params.taskId,
}),
connect(null, { goBack }),
)(TaskModalInner);
export default TaskModal;
import { goBack } from "react-router-redux";
import { t } from "ttag";
import { useGetTaskQuery } from "metabase/api";
import Code from "metabase/components/Code";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper";
import ModalContent from "metabase/components/ModalContent";
import { useDispatch } from "metabase/lib/redux";
type TaskModalProps = {
params: { taskId: number };
};
export const TaskModal = ({ params }: TaskModalProps) => {
const dispatch = useDispatch();
const { data, isLoading, error } = useGetTaskQuery(params.taskId);
if (isLoading || error) {
return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
}
if (!data) {
return null;
}
return (
<ModalContent title={t`Task details`} onClose={() => dispatch(goBack())}>
<Code>{JSON.stringify(data.task_details)}</Code>
</ModalContent>
);
};
/* eslint-disable react/prop-types */
import cx from "classnames";
import { Component } from "react";
import { t } from "ttag";
import _ from "underscore";
import AdminHeader from "metabase/components/AdminHeader";
import PaginationControls from "metabase/components/PaginationControls";
import Link from "metabase/core/components/Link";
import Tooltip from "metabase/core/components/Tooltip";
import AdminS from "metabase/css/admin.module.css";
import CS from "metabase/css/core/index.css";
import Database from "metabase/entities/databases";
import Task from "metabase/entities/tasks";
import {
InfoIcon,
SectionControls,
SectionHeader,
SectionRoot,
SectionTitle,
} from "./TasksApp.styled";
// Please preserve the following 2 @ calls in this order.
// Otherwise @Database.loadList overrides pagination props
// that come from @Task.LoadList
class TasksAppInner extends Component {
render() {
const {
tasks,
databases,
page,
pageSize,
onNextPage,
onPreviousPage,
children,
} = this.props;
const databaseByID = {};
// index databases by id for lookup
for (const db of databases) {
databaseByID[db.id] = db;
}
return (
<SectionRoot>
<SectionHeader>
<SectionTitle>
<AdminHeader title={t`Troubleshooting logs`} />
<Tooltip
tooltip={t`Trying to get to the bottom of something? This section shows logs of Metabase's background tasks, which can help shed light on what's going on.`}
>
<InfoIcon name="info" />
</Tooltip>
</SectionTitle>
<SectionControls>
<PaginationControls
onPreviousPage={onPreviousPage}
onNextPage={onNextPage}
page={page}
pageSize={pageSize}
itemsLength={tasks.length}
/>
</SectionControls>
</SectionHeader>
<table className={cx(AdminS.ContentTable, CS.mt2)}>
<thead>
<tr>
<th>{t`Task`}</th>
<th>{t`DB Name`}</th>
<th>{t`DB Engine`}</th>
<th>{t`Started at`}</th>
<th>{t`Ended at`}</th>
<th>{t`Duration (ms)`}</th>
<th>{t`Details`}</th>
</tr>
</thead>
<tbody>
{tasks.map(task => {
const db = task.db_id ? databaseByID[task.db_id] : null;
const name = db ? db.name : null;
const engine = db ? db.engine : null;
// only want unknown if there is a db on the task and we don't have info
return (
<tr key={task.id}>
<td className={CS.textBold}>{task.task}</td>
<td>{task.db_id ? name || t`Unknown name` : null}</td>
<td>{task.db_id ? engine || t`Unknown engine` : null}</td>
<td>{task.started_at}</td>
<td>{task.ended_at}</td>
<td>{task.duration}</td>
<td>
<Link
className={cx(CS.link, CS.textBold)}
to={`/admin/troubleshooting/tasks/${task.id}`}
>{t`View`}</Link>
</td>
</tr>
);
})}
</tbody>
</table>
{
// render 'children' so that the invididual task modals show up
children
}
</SectionRoot>
);
}
}
const TasksApp = _.compose(
Database.loadList(),
Task.loadList({
pageSize: 50,
}),
)(TasksAppInner);
export default TasksApp;
import cx from "classnames";
import type { ReactNode } from "react";
import { useState } from "react";
import { t } from "ttag";
import _ from "underscore";
import { useListTasksQuery, useListDatabasesQuery } from "metabase/api";
import AdminHeader from "metabase/components/AdminHeader";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper";
import PaginationControls from "metabase/components/PaginationControls";
import Link from "metabase/core/components/Link";
import AdminS from "metabase/css/admin.module.css";
import CS from "metabase/css/core/index.css";
import { Tooltip } from "metabase/ui";
import type { Database, Task } from "metabase-types/api";
import {
InfoIcon,
SectionControls,
SectionHeader,
SectionRoot,
SectionTitle,
} from "./TasksApp.styled";
type TasksAppProps = {
children: ReactNode;
};
export const TasksApp = ({ children }: TasksAppProps) => {
const [page, setPage] = useState(0);
const pageSize = 50;
const {
data: tasksData,
isFetching: isLoadingTasks,
error: tasksError,
} = useListTasksQuery({
limit: pageSize,
offset: page * pageSize,
});
const {
data: databasesData,
isFetching: isLoadingDatabases,
error: databasesError,
} = useListDatabasesQuery();
const tasks = tasksData?.data;
const databases = databasesData?.data;
if (isLoadingTasks || isLoadingDatabases || tasksError || databasesError) {
return (
<LoadingAndErrorWrapper
loading={isLoadingTasks || isLoadingDatabases}
error={tasksError || databasesError}
/>
);
}
if (!tasks || !databases) {
return null;
}
// index databases by id for lookup
const databaseByID: Record<number, Database> = _.indexBy(databases, "id");
return (
<SectionRoot>
<SectionHeader>
<SectionTitle>
<AdminHeader title={t`Troubleshooting logs`} />
<Tooltip
label={t`Trying to get to the bottom of something? This section shows logs of Metabase's background tasks, which can help shed light on what's going on.`}
>
<InfoIcon name="info" />
</Tooltip>
</SectionTitle>
<SectionControls>
<PaginationControls
onPreviousPage={() => setPage(page - 1)}
onNextPage={() => setPage(page + 1)}
page={page}
pageSize={50}
itemsLength={tasks.length}
total={tasksData.total}
/>
</SectionControls>
</SectionHeader>
<table className={cx(AdminS.ContentTable, CS.mt2)}>
<thead>
<tr>
<th>{t`Task`}</th>
<th>{t`DB Name`}</th>
<th>{t`DB Engine`}</th>
<th>{t`Started at`}</th>
<th>{t`Ended at`}</th>
<th>{t`Duration (ms)`}</th>
<th>{t`Details`}</th>
</tr>
</thead>
<tbody>
{tasks.map((task: Task) => {
const db = task.db_id ? databaseByID[task.db_id] : null;
const name = db ? db.name : null;
const engine = db ? db.engine : null;
// only want unknown if there is a db on the task and we don't have info
return (
<tr key={task.id}>
<td className={CS.textBold}>{task.task}</td>
<td>{task.db_id ? name || t`Unknown name` : null}</td>
<td>{task.db_id ? engine || t`Unknown engine` : null}</td>
<td>{task.started_at}</td>
<td>{task.ended_at}</td>
<td>{task.duration}</td>
<td>
<Link
className={cx(CS.link, CS.textBold)}
to={`/admin/troubleshooting/tasks/${task.id}`}
>{t`View`}</Link>
</td>
</tr>
);
})}
</tbody>
</table>
{
// render 'children' so that the invididual task modals show up
children
}
</SectionRoot>
);
};
......@@ -16,6 +16,7 @@ export * from "./search";
export * from "./segment";
export * from "./session";
export * from "./table";
export * from "./task";
export * from "./timeline";
export * from "./timeline-event";
export * from "./user";
......@@ -17,6 +17,7 @@ export const TAG_TYPES = [
"snippet",
"segment",
"table",
"task",
"timeline",
"timeline-event",
"user",
......
......@@ -23,6 +23,7 @@ import type {
SearchResult,
Segment,
Table,
Task,
Timeline,
TimelineEvent,
UserInfo,
......@@ -317,6 +318,14 @@ export function provideTableTags(table: Table): TagDescription<TagType>[] {
];
}
export function provideTaskListTags(tasks: Task[]): TagDescription<TagType>[] {
return [listTag("task"), ...tasks.flatMap(provideTaskTags)];
}
export function provideTaskTags(task: Task): TagDescription<TagType>[] {
return [idTag("task", task.id)];
}
export function provideTimelineEventListTags(
events: TimelineEvent[],
): TagDescription<TagType>[] {
......
import type {
ListTasksRequest,
ListTasksResponse,
Task,
} from "metabase-types/api";
import { Api } from "./api";
import { provideTaskTags, provideTaskListTags } from "./tags";
export const taskApi = Api.injectEndpoints({
endpoints: builder => ({
listTasks: builder.query<ListTasksResponse, ListTasksRequest | void>({
query: params => ({
method: "GET",
url: "/api/task",
params,
}),
providesTags: response =>
response ? provideTaskListTags(response.data) : [],
}),
getTask: builder.query<Task, number>({
query: id => ({
method: "GET",
url: `/api/task/${id}`,
}),
providesTags: task => (task ? provideTaskTags(task) : []),
}),
getTasksInfo: builder.query<unknown, void>({
query: () => ({
method: "GET",
url: "/api/task/info",
}),
}),
}),
});
export const { useListTasksQuery, useGetTaskQuery, useGetTasksInfoQuery } =
taskApi;
......@@ -21,6 +21,9 @@ export default function PaginationControls({
return null;
}
const isLastPage = (pageIndex, pageSize, total) =>
pageIndex === Math.ceil(total / pageSize) - 1;
const isPreviousDisabled = page === 0;
const isNextDisabled =
total != null ? isLastPage(page, pageSize, total) : !onNextPage;
......@@ -73,6 +76,3 @@ PaginationControls.propTypes = {
PaginationControls.defaultProps = {
showTotal: false,
};
export const isLastPage = (pageIndex, pageSize, total) =>
pageIndex === Math.ceil(total / pageSize) - 1;
......@@ -20,7 +20,6 @@ export { default as tables } from "./tables";
export { default as fields } from "./fields";
export { default as metrics } from "./metrics";
export { default as segments } from "./segments";
export { default as tasks } from "./tasks";
export { default as users } from "./users";
export { default as groups } from "./groups";
......
import { createEntity } from "metabase/lib/entities";
/**
* @deprecated use "metabase/api" instead
*/
export default createEntity({
name: "tasks",
path: "/api/task",
});
......@@ -470,7 +470,6 @@ export const I18NApi = {
};
export const TaskApi = {
get: GET("/api/task"),
getJobsInfo: GET("/api/task/info"),
};
......
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