mirror of
https://github.com/hibiken/asynqmon.git
synced 2025-01-19 03:05:53 +08:00
Add Task details view
Allow users to find task by task ID
This commit is contained in:
parent
d63e4a3229
commit
3befee382d
@ -85,6 +85,61 @@ func toDailyStatsList(in []*asynq.DailyStats) []*DailyStats {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskInfo struct {
|
||||||
|
// ID is the identifier of the task.
|
||||||
|
ID string `json:"id"`
|
||||||
|
// Queue is the name of the queue in which the task belongs.
|
||||||
|
Queue string `json:"queue"`
|
||||||
|
// Type is the type name of the task.
|
||||||
|
Type string `json:"type"`
|
||||||
|
// Payload is the payload data of the task.
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
// State indicates the task state.
|
||||||
|
State string `json:"state"`
|
||||||
|
// MaxRetry is the maximum number of times the task can be retried.
|
||||||
|
MaxRetry int `json:"max_retry"`
|
||||||
|
// Retried is the number of times the task has retried so far.
|
||||||
|
Retried int `json:"retried"`
|
||||||
|
// LastErr is the error message from the last failure.
|
||||||
|
LastErr string `json:"error_message"`
|
||||||
|
// LastFailedAt is the time time of the last failure in RFC3339 format.
|
||||||
|
// If the task has no failures, empty string.
|
||||||
|
LastFailedAt string `json:"last_failed_at"`
|
||||||
|
// Timeout is the number of seconds the task can be processed by Handler before being retried.
|
||||||
|
Timeout int `json:"timeout_seconds"`
|
||||||
|
// Deadline is the deadline for the task in RFC3339 format. If not set, empty string.
|
||||||
|
Deadline string `json:"deadline"`
|
||||||
|
// NextProcessAt is the time the task is scheduled to be processed in RFC3339 format.
|
||||||
|
// If not applicable, empty string.
|
||||||
|
NextProcessAt string `json:"next_process_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTimeInRFC3339 formats t in RFC3339 if the value is non-zero.
|
||||||
|
// If t is zero time (i.e. time.Time{}), returns empty string
|
||||||
|
func formatTimeInRFC3339(t time.Time) string {
|
||||||
|
if t.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toTaskInfo(info *asynq.TaskInfo) *TaskInfo {
|
||||||
|
return &TaskInfo{
|
||||||
|
ID: info.ID,
|
||||||
|
Queue: info.Queue,
|
||||||
|
Type: info.Type,
|
||||||
|
Payload: toPrintablePayload(info.Payload),
|
||||||
|
State: info.State.String(),
|
||||||
|
MaxRetry: info.MaxRetry,
|
||||||
|
Retried: info.Retried,
|
||||||
|
LastErr: info.LastErr,
|
||||||
|
LastFailedAt: formatTimeInRFC3339(info.LastFailedAt),
|
||||||
|
Timeout: int(info.Timeout.Seconds()),
|
||||||
|
Deadline: formatTimeInRFC3339(info.Deadline),
|
||||||
|
NextProcessAt: formatTimeInRFC3339(info.NextProcessAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type BaseTask struct {
|
type BaseTask struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
2
main.go
2
main.go
@ -197,6 +197,8 @@ func main() {
|
|||||||
api.HandleFunc("/queues/{qname}/archived_tasks:run_all", newRunAllArchivedTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/archived_tasks:run_all", newRunAllArchivedTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
api.HandleFunc("/queues/{qname}/archived_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/archived_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
|
|
||||||
|
api.HandleFunc("/queues/{qname}/tasks/{task_id}", newGetTaskHandlerFunc(inspector)).Methods("GET")
|
||||||
|
|
||||||
// Servers endpoints.
|
// Servers endpoints.
|
||||||
api.HandleFunc("/servers", newListServersHandlerFunc(inspector)).Methods("GET")
|
api.HandleFunc("/servers", newListServersHandlerFunc(inspector)).Methods("GET")
|
||||||
|
|
||||||
|
@ -2,9 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -623,3 +625,33 @@ func getPageOptions(r *http.Request) (pageSize, pageNum int) {
|
|||||||
}
|
}
|
||||||
return pageSize, pageNum
|
return pageSize, pageNum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newGetTaskHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
qname, taskid := vars["qname"], vars["task_id"]
|
||||||
|
if qname == "" {
|
||||||
|
http.Error(w, "queue name cannot be empty", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if taskid == "" {
|
||||||
|
http.Error(w, "task_id cannot be empty", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := inspector.GetTaskInfo(qname, taskid)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, asynq.ErrQueueNotFound), errors.Is(err, asynq.ErrTaskNotFound):
|
||||||
|
http.Error(w, strings.TrimPrefix(err.Error(), "asynq: "), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
case err != nil:
|
||||||
|
http.Error(w, strings.TrimPrefix(err.Error(), "asynq: "), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(toTaskInfo(info)); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -33,6 +33,7 @@ import ListItemLink from "./components/ListItemLink";
|
|||||||
import SchedulersView from "./views/SchedulersView";
|
import SchedulersView from "./views/SchedulersView";
|
||||||
import DashboardView from "./views/DashboardView";
|
import DashboardView from "./views/DashboardView";
|
||||||
import TasksView from "./views/TasksView";
|
import TasksView from "./views/TasksView";
|
||||||
|
import TaskDetailsView from "./views/TaskDetailsView";
|
||||||
import SettingsView from "./views/SettingsView";
|
import SettingsView from "./views/SettingsView";
|
||||||
import ServersView from "./views/ServersView";
|
import ServersView from "./views/ServersView";
|
||||||
import RedisInfoView from "./views/RedisInfoView";
|
import RedisInfoView from "./views/RedisInfoView";
|
||||||
@ -269,6 +270,9 @@ function App(props: ConnectedProps<typeof connector>) {
|
|||||||
<main className={classes.content}>
|
<main className={classes.content}>
|
||||||
<div className={classes.contentWrapper}>
|
<div className={classes.contentWrapper}>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Route exact path={paths.TASK_DETAILS}>
|
||||||
|
<TaskDetailsView />
|
||||||
|
</Route>
|
||||||
<Route exact path={paths.QUEUE_DETAILS}>
|
<Route exact path={paths.QUEUE_DETAILS}>
|
||||||
<TasksView />
|
<TasksView />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -47,11 +47,16 @@ import {
|
|||||||
archivePendingTask,
|
archivePendingTask,
|
||||||
batchArchivePendingTasks,
|
batchArchivePendingTasks,
|
||||||
archiveAllPendingTasks,
|
archiveAllPendingTasks,
|
||||||
|
TaskInfo,
|
||||||
|
getTaskInfo,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
|
import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
|
||||||
|
|
||||||
// List of tasks related action types.
|
// List of tasks related action types.
|
||||||
|
export const GET_TASK_INFO_BEGIN = "GET_TASK_INFO_BEGIN";
|
||||||
|
export const GET_TASK_INFO_SUCCESS = "GET_TASK_INFO_SUCCESS";
|
||||||
|
export const GET_TASK_INFO_ERROR = "GET_TASK_INFO_ERROR";
|
||||||
export const LIST_ACTIVE_TASKS_BEGIN = "LIST_ACTIVE_TASKS_BEGIN";
|
export const LIST_ACTIVE_TASKS_BEGIN = "LIST_ACTIVE_TASKS_BEGIN";
|
||||||
export const LIST_ACTIVE_TASKS_SUCCESS = "LIST_ACTIVE_TASKS_SUCCESS";
|
export const LIST_ACTIVE_TASKS_SUCCESS = "LIST_ACTIVE_TASKS_SUCCESS";
|
||||||
export const LIST_ACTIVE_TASKS_ERROR = "LIST_ACTIVE_TASKS_ERROR";
|
export const LIST_ACTIVE_TASKS_ERROR = "LIST_ACTIVE_TASKS_ERROR";
|
||||||
@ -80,9 +85,9 @@ export const BATCH_CANCEL_ACTIVE_TASKS_SUCCESS =
|
|||||||
"BATCH_CANCEL_ACTIVE_TASKS_SUCCESS";
|
"BATCH_CANCEL_ACTIVE_TASKS_SUCCESS";
|
||||||
export const BATCH_CANCEL_ACTIVE_TASKS_ERROR =
|
export const BATCH_CANCEL_ACTIVE_TASKS_ERROR =
|
||||||
"BATCH_CANCEL_ACTIVE_TASKS_ERROR";
|
"BATCH_CANCEL_ACTIVE_TASKS_ERROR";
|
||||||
export const RUN_SCHEDULED_TASK_BEGIN = "RUN_ARCHIVED_TASK_BEGIN";
|
export const RUN_SCHEDULED_TASK_BEGIN = "RUN_SCHEDULED_TASK_BEGIN";
|
||||||
export const RUN_SCHEDULED_TASK_SUCCESS = "RUN_ARCHIVED_TASK_SUCCESS";
|
export const RUN_SCHEDULED_TASK_SUCCESS = "RUN_SCHEDULED_TASK_SUCCESS";
|
||||||
export const RUN_SCHEDULED_TASK_ERROR = "RUN_ARCHIVED_TASK_ERROR";
|
export const RUN_SCHEDULED_TASK_ERROR = "RUN_SCHEDULED_TASK_ERROR";
|
||||||
export const RUN_RETRY_TASK_BEGIN = "RUN_RETRY_TASK_BEGIN";
|
export const RUN_RETRY_TASK_BEGIN = "RUN_RETRY_TASK_BEGIN";
|
||||||
export const RUN_RETRY_TASK_SUCCESS = "RUN_RETRY_TASK_SUCCESS";
|
export const RUN_RETRY_TASK_SUCCESS = "RUN_RETRY_TASK_SUCCESS";
|
||||||
export const RUN_RETRY_TASK_ERROR = "RUN_RETRY_TASK_ERROR";
|
export const RUN_RETRY_TASK_ERROR = "RUN_RETRY_TASK_ERROR";
|
||||||
@ -209,6 +214,20 @@ export const DELETE_ALL_ARCHIVED_TASKS_SUCCESS =
|
|||||||
export const DELETE_ALL_ARCHIVED_TASKS_ERROR =
|
export const DELETE_ALL_ARCHIVED_TASKS_ERROR =
|
||||||
"DELETE_ALL_ARCHIVED_TASKS_ERROR";
|
"DELETE_ALL_ARCHIVED_TASKS_ERROR";
|
||||||
|
|
||||||
|
interface GetTaskInfoBeginAction {
|
||||||
|
type: typeof GET_TASK_INFO_BEGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetTaskInfoErrorAction {
|
||||||
|
type: typeof GET_TASK_INFO_ERROR;
|
||||||
|
error: string; // error description
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetTaskInfoSuccessAction {
|
||||||
|
type: typeof GET_TASK_INFO_SUCCESS;
|
||||||
|
payload: TaskInfo;
|
||||||
|
}
|
||||||
|
|
||||||
interface ListActiveTasksBeginAction {
|
interface ListActiveTasksBeginAction {
|
||||||
type: typeof LIST_ACTIVE_TASKS_BEGIN;
|
type: typeof LIST_ACTIVE_TASKS_BEGIN;
|
||||||
queue: string;
|
queue: string;
|
||||||
@ -894,6 +913,9 @@ interface DeleteAllArchivedTasksErrorAction {
|
|||||||
|
|
||||||
// Union of all tasks related action types.
|
// Union of all tasks related action types.
|
||||||
export type TasksActionTypes =
|
export type TasksActionTypes =
|
||||||
|
| GetTaskInfoBeginAction
|
||||||
|
| GetTaskInfoErrorAction
|
||||||
|
| GetTaskInfoSuccessAction
|
||||||
| ListActiveTasksBeginAction
|
| ListActiveTasksBeginAction
|
||||||
| ListActiveTasksSuccessAction
|
| ListActiveTasksSuccessAction
|
||||||
| ListActiveTasksErrorAction
|
| ListActiveTasksErrorAction
|
||||||
@ -1009,6 +1031,25 @@ export type TasksActionTypes =
|
|||||||
| DeleteAllArchivedTasksSuccessAction
|
| DeleteAllArchivedTasksSuccessAction
|
||||||
| DeleteAllArchivedTasksErrorAction;
|
| DeleteAllArchivedTasksErrorAction;
|
||||||
|
|
||||||
|
export function getTaskInfoAsync(qname: string, id: string) {
|
||||||
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
|
dispatch({ type: GET_TASK_INFO_BEGIN });
|
||||||
|
try {
|
||||||
|
const response = await getTaskInfo(qname, id);
|
||||||
|
dispatch({
|
||||||
|
type: GET_TASK_INFO_SUCCESS,
|
||||||
|
payload: response,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("getTaskInfoAsync: ", toErrorStringWithHttpStatus(error));
|
||||||
|
dispatch({
|
||||||
|
type: GET_TASK_INFO_ERROR,
|
||||||
|
error: toErrorString(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function listActiveTasksAsync(
|
export function listActiveTasksAsync(
|
||||||
qname: string,
|
qname: string,
|
||||||
pageOpts?: PaginationOptions
|
pageOpts?: PaginationOptions
|
||||||
|
@ -245,6 +245,21 @@ interface BaseTask {
|
|||||||
payload: string;
|
payload: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TaskInfo {
|
||||||
|
id: string;
|
||||||
|
queue: string;
|
||||||
|
type: string;
|
||||||
|
payload: string;
|
||||||
|
state: string;
|
||||||
|
max_retry: number;
|
||||||
|
retried: number;
|
||||||
|
last_failed_at: string;
|
||||||
|
error_message: string;
|
||||||
|
next_process_at: string;
|
||||||
|
timeout_seconds: number;
|
||||||
|
deadline: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActiveTask extends BaseTask {
|
export interface ActiveTask extends BaseTask {
|
||||||
id: string;
|
id: string;
|
||||||
queue: string;
|
queue: string;
|
||||||
@ -369,6 +384,15 @@ export async function listQueueStats(): Promise<ListQueueStatsResponse> {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTaskInfo(qname: string, id: string): Promise<TaskInfo> {
|
||||||
|
const url = `${BASE_URL}/queues/${qname}/tasks/${id}`;
|
||||||
|
const resp = await axios({
|
||||||
|
method: "get",
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listActiveTasks(
|
export async function listActiveTasks(
|
||||||
qname: string,
|
qname: string,
|
||||||
pageOpts?: PaginationOptions
|
pageOpts?: PaginationOptions
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useCallback } from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
import { connect, ConnectedProps } from "react-redux";
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import Table from "@material-ui/core/Table";
|
import Table from "@material-ui/core/Table";
|
||||||
@ -34,6 +35,7 @@ import { usePolling } from "../hooks";
|
|||||||
import { ActiveTaskExtended } from "../reducers/tasksReducer";
|
import { ActiveTaskExtended } from "../reducers/tasksReducer";
|
||||||
import { durationBefore, timeAgo, uuidPrefix } from "../utils";
|
import { durationBefore, timeAgo, uuidPrefix } from "../utils";
|
||||||
import { TableColumn } from "../types/table";
|
import { TableColumn } from "../types/table";
|
||||||
|
import { taskDetailsPath } from "../paths";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
table: {
|
table: {
|
||||||
@ -189,14 +191,16 @@ function ActiveTasksTable(props: Props & ReduxProps) {
|
|||||||
padding="checkbox"
|
padding="checkbox"
|
||||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
<Checkbox
|
||||||
checked={rowCount > 0 && numSelected === rowCount}
|
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||||
onChange={handleSelectAllClick}
|
checked={rowCount > 0 && numSelected === rowCount}
|
||||||
inputProps={{
|
onChange={handleSelectAllClick}
|
||||||
"aria-label": "select all tasks shown in the table",
|
inputProps={{
|
||||||
}}
|
"aria-label": "select all tasks shown in the table",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
@ -257,6 +261,18 @@ function ActiveTasksTable(props: Props & ReduxProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useRowStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: theme.shadows[2],
|
||||||
|
},
|
||||||
|
"&:hover .MuiTableCell-root": {
|
||||||
|
borderBottomColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
task: ActiveTaskExtended;
|
task: ActiveTaskExtended;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
@ -269,15 +285,24 @@ interface RowProps {
|
|||||||
|
|
||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
const { task } = props;
|
const { task } = props;
|
||||||
|
const classes = useRowStyles();
|
||||||
|
const history = useHistory();
|
||||||
return (
|
return (
|
||||||
<TableRow key={task.id} selected={props.isSelected}>
|
<TableRow
|
||||||
<TableCell padding="checkbox">
|
key={task.id}
|
||||||
<Checkbox
|
className={classes.root}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
selected={props.isSelected}
|
||||||
props.onSelectChange(event.target.checked)
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
}
|
>
|
||||||
checked={props.isSelected}
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
/>
|
<IconButton>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
props.onSelectChange(event.target.checked)
|
||||||
|
}
|
||||||
|
checked={props.isSelected}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell component="th" scope="row">
|
||||||
{uuidPrefix(task.id)}
|
{uuidPrefix(task.id)}
|
||||||
@ -302,6 +327,7 @@ function Row(props: RowProps) {
|
|||||||
align="center"
|
align="center"
|
||||||
onMouseEnter={props.onActionCellEnter}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{props.showActions ? (
|
{props.showActions ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
import { connect, ConnectedProps } from "react-redux";
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import Table from "@material-ui/core/Table";
|
import Table from "@material-ui/core/Table";
|
||||||
@ -38,6 +39,7 @@ import { timeAgo, uuidPrefix } from "../utils";
|
|||||||
import { usePolling } from "../hooks";
|
import { usePolling } from "../hooks";
|
||||||
import { ArchivedTaskExtended } from "../reducers/tasksReducer";
|
import { ArchivedTaskExtended } from "../reducers/tasksReducer";
|
||||||
import { TableColumn } from "../types/table";
|
import { TableColumn } from "../types/table";
|
||||||
|
import { taskDetailsPath } from "../paths";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
table: {
|
table: {
|
||||||
@ -216,14 +218,16 @@ function ArchivedTasksTable(props: Props & ReduxProps) {
|
|||||||
padding="checkbox"
|
padding="checkbox"
|
||||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
<Checkbox
|
||||||
checked={rowCount > 0 && numSelected === rowCount}
|
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||||
onChange={handleSelectAllClick}
|
checked={rowCount > 0 && numSelected === rowCount}
|
||||||
inputProps={{
|
onChange={handleSelectAllClick}
|
||||||
"aria-label": "select all tasks shown in the table",
|
inputProps={{
|
||||||
}}
|
"aria-label": "select all tasks shown in the table",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
@ -288,6 +292,15 @@ function ArchivedTasksTable(props: Props & ReduxProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useRowStyles = makeStyles((theme) => ({
|
const useRowStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: theme.shadows[2],
|
||||||
|
},
|
||||||
|
"&:hover .MuiTableCell-root": {
|
||||||
|
borderBottomColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
},
|
||||||
actionCell: {
|
actionCell: {
|
||||||
width: "96px",
|
width: "96px",
|
||||||
},
|
},
|
||||||
@ -312,15 +325,23 @@ interface RowProps {
|
|||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
const { task } = props;
|
const { task } = props;
|
||||||
const classes = useRowStyles();
|
const classes = useRowStyles();
|
||||||
|
const history = useHistory();
|
||||||
return (
|
return (
|
||||||
<TableRow key={task.id} selected={props.isSelected}>
|
<TableRow
|
||||||
<TableCell padding="checkbox">
|
key={task.id}
|
||||||
<Checkbox
|
className={classes.root}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
selected={props.isSelected}
|
||||||
props.onSelectChange(event.target.checked)
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
}
|
>
|
||||||
checked={props.isSelected}
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
/>
|
<IconButton>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
props.onSelectChange(event.target.checked)
|
||||||
|
}
|
||||||
|
checked={props.isSelected}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell component="th" scope="row">
|
||||||
{uuidPrefix(task.id)}
|
{uuidPrefix(task.id)}
|
||||||
@ -341,6 +362,7 @@ function Row(props: RowProps) {
|
|||||||
className={classes.actionCell}
|
className={classes.actionCell}
|
||||||
onMouseEnter={props.onActionCellEnter}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{props.showActions ? (
|
{props.showActions ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
import { connect, ConnectedProps } from "react-redux";
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import Table from "@material-ui/core/Table";
|
import Table from "@material-ui/core/Table";
|
||||||
@ -38,6 +39,7 @@ import { usePolling } from "../hooks";
|
|||||||
import { uuidPrefix } from "../utils";
|
import { uuidPrefix } from "../utils";
|
||||||
import { TableColumn } from "../types/table";
|
import { TableColumn } from "../types/table";
|
||||||
import { PendingTaskExtended } from "../reducers/tasksReducer";
|
import { PendingTaskExtended } from "../reducers/tasksReducer";
|
||||||
|
import { taskDetailsPath } from "../paths";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
table: {
|
table: {
|
||||||
@ -216,14 +218,16 @@ function PendingTasksTable(props: Props & ReduxProps) {
|
|||||||
padding="checkbox"
|
padding="checkbox"
|
||||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
<Checkbox
|
||||||
checked={rowCount > 0 && numSelected === rowCount}
|
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||||
onChange={handleSelectAllClick}
|
checked={rowCount > 0 && numSelected === rowCount}
|
||||||
inputProps={{
|
onChange={handleSelectAllClick}
|
||||||
"aria-label": "select all tasks shown in the table",
|
inputProps={{
|
||||||
}}
|
"aria-label": "select all tasks shown in the table",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
@ -289,7 +293,16 @@ function PendingTasksTable(props: Props & ReduxProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRowStyles = makeStyles({
|
const useRowStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: theme.shadows[2],
|
||||||
|
},
|
||||||
|
"&:hover .MuiTableCell-root": {
|
||||||
|
borderBottomColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
},
|
||||||
actionCell: {
|
actionCell: {
|
||||||
width: "96px",
|
width: "96px",
|
||||||
},
|
},
|
||||||
@ -297,7 +310,7 @@ const useRowStyles = makeStyles({
|
|||||||
marginLeft: 3,
|
marginLeft: 3,
|
||||||
marginRight: 3,
|
marginRight: 3,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
task: PendingTaskExtended;
|
task: PendingTaskExtended;
|
||||||
@ -314,15 +327,23 @@ interface RowProps {
|
|||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
const { task } = props;
|
const { task } = props;
|
||||||
const classes = useRowStyles();
|
const classes = useRowStyles();
|
||||||
|
const history = useHistory();
|
||||||
return (
|
return (
|
||||||
<TableRow key={task.id} selected={props.isSelected}>
|
<TableRow
|
||||||
<TableCell padding="checkbox">
|
key={task.id}
|
||||||
<Checkbox
|
className={classes.root}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
selected={props.isSelected}
|
||||||
props.onSelectChange(event.target.checked)
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
}
|
>
|
||||||
checked={props.isSelected}
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
/>
|
<IconButton>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
props.onSelectChange(event.target.checked)
|
||||||
|
}
|
||||||
|
checked={props.isSelected}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell component="th" scope="row">
|
||||||
{uuidPrefix(task.id)}
|
{uuidPrefix(task.id)}
|
||||||
@ -343,6 +364,7 @@ function Row(props: RowProps) {
|
|||||||
className={classes.actionCell}
|
className={classes.actionCell}
|
||||||
onMouseEnter={props.onActionCellEnter}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{props.showActions ? (
|
{props.showActions ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -31,7 +31,9 @@ interface Props {
|
|||||||
// All queue names.
|
// All queue names.
|
||||||
queues: string[];
|
queues: string[];
|
||||||
// Name of the queue currently selected.
|
// Name of the queue currently selected.
|
||||||
selectedQueue: string;
|
queueName: string;
|
||||||
|
// ID of the task currently selected (optional).
|
||||||
|
taskId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QueueBreadcrumbs(props: Props) {
|
export default function QueueBreadcrumbs(props: Props) {
|
||||||
@ -57,11 +59,12 @@ export default function QueueBreadcrumbs(props: Props) {
|
|||||||
onClick={() => history.push(paths.HOME)}
|
onClick={() => history.push(paths.HOME)}
|
||||||
/>
|
/>
|
||||||
<StyledBreadcrumb
|
<StyledBreadcrumb
|
||||||
label={props.selectedQueue}
|
label={props.queueName}
|
||||||
deleteIcon={<ExpandMoreIcon />}
|
deleteIcon={<ExpandMoreIcon />}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDelete={handleClick}
|
onDelete={handleClick}
|
||||||
/>
|
/>
|
||||||
|
{props.taskId && <StyledBreadcrumb label={`task:${props.taskId}`} />}
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<Menu
|
<Menu
|
||||||
id="queue-breadcrumb-menu"
|
id="queue-breadcrumb-menu"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
import { connect, ConnectedProps } from "react-redux";
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import Table from "@material-ui/core/Table";
|
import Table from "@material-ui/core/Table";
|
||||||
@ -42,6 +43,7 @@ import { durationBefore, uuidPrefix } from "../utils";
|
|||||||
import { usePolling } from "../hooks";
|
import { usePolling } from "../hooks";
|
||||||
import { RetryTaskExtended } from "../reducers/tasksReducer";
|
import { RetryTaskExtended } from "../reducers/tasksReducer";
|
||||||
import { TableColumn } from "../types/table";
|
import { TableColumn } from "../types/table";
|
||||||
|
import { taskDetailsPath } from "../paths";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
table: {
|
table: {
|
||||||
@ -246,14 +248,16 @@ function RetryTasksTable(props: Props & ReduxProps) {
|
|||||||
padding="checkbox"
|
padding="checkbox"
|
||||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
<Checkbox
|
||||||
checked={rowCount > 0 && numSelected === rowCount}
|
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||||
onChange={handleSelectAllClick}
|
checked={rowCount > 0 && numSelected === rowCount}
|
||||||
inputProps={{
|
onChange={handleSelectAllClick}
|
||||||
"aria-label": "select all tasks shown in the table",
|
inputProps={{
|
||||||
}}
|
"aria-label": "select all tasks shown in the table",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
@ -320,7 +324,16 @@ function RetryTasksTable(props: Props & ReduxProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRowStyles = makeStyles({
|
const useRowStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: theme.shadows[2],
|
||||||
|
},
|
||||||
|
"&:hover .MuiTableCell-root": {
|
||||||
|
borderBottomColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
},
|
||||||
actionCell: {
|
actionCell: {
|
||||||
width: "140px",
|
width: "140px",
|
||||||
},
|
},
|
||||||
@ -328,7 +341,7 @@ const useRowStyles = makeStyles({
|
|||||||
marginLeft: 3,
|
marginLeft: 3,
|
||||||
marginRight: 3,
|
marginRight: 3,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
task: RetryTaskExtended;
|
task: RetryTaskExtended;
|
||||||
@ -346,15 +359,24 @@ interface RowProps {
|
|||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
const { task } = props;
|
const { task } = props;
|
||||||
const classes = useRowStyles();
|
const classes = useRowStyles();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={task.id} selected={props.isSelected}>
|
<TableRow
|
||||||
<TableCell padding="checkbox">
|
key={task.id}
|
||||||
<Checkbox
|
className={classes.root}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
selected={props.isSelected}
|
||||||
props.onSelectChange(event.target.checked)
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
}
|
>
|
||||||
checked={props.isSelected}
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
/>
|
<IconButton>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
props.onSelectChange(event.target.checked)
|
||||||
|
}
|
||||||
|
checked={props.isSelected}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell component="th" scope="row">
|
||||||
{uuidPrefix(task.id)}
|
{uuidPrefix(task.id)}
|
||||||
@ -377,6 +399,7 @@ function Row(props: RowProps) {
|
|||||||
className={classes.actionCell}
|
className={classes.actionCell}
|
||||||
onMouseEnter={props.onActionCellEnter}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{props.showActions ? (
|
{props.showActions ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useCallback } from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
import { connect, ConnectedProps } from "react-redux";
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import Table from "@material-ui/core/Table";
|
import Table from "@material-ui/core/Table";
|
||||||
@ -42,6 +43,7 @@ import { durationBefore, uuidPrefix } from "../utils";
|
|||||||
import { usePolling } from "../hooks";
|
import { usePolling } from "../hooks";
|
||||||
import { ScheduledTaskExtended } from "../reducers/tasksReducer";
|
import { ScheduledTaskExtended } from "../reducers/tasksReducer";
|
||||||
import { TableColumn } from "../types/table";
|
import { TableColumn } from "../types/table";
|
||||||
|
import { taskDetailsPath } from "../paths";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
table: {
|
table: {
|
||||||
@ -243,14 +245,16 @@ function ScheduledTasksTable(props: Props & ReduxProps) {
|
|||||||
padding="checkbox"
|
padding="checkbox"
|
||||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
<Checkbox
|
||||||
checked={rowCount > 0 && numSelected === rowCount}
|
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||||
onChange={handleSelectAllClick}
|
checked={rowCount > 0 && numSelected === rowCount}
|
||||||
inputProps={{
|
onChange={handleSelectAllClick}
|
||||||
"aria-label": "select all tasks shown in the table",
|
inputProps={{
|
||||||
}}
|
"aria-label": "select all tasks shown in the table",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
@ -317,7 +321,16 @@ function ScheduledTasksTable(props: Props & ReduxProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRowStyles = makeStyles({
|
const useRowStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: theme.shadows[2],
|
||||||
|
},
|
||||||
|
"&:hover .MuiTableCell-root": {
|
||||||
|
borderBottomColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
},
|
||||||
actionCell: {
|
actionCell: {
|
||||||
width: "140px",
|
width: "140px",
|
||||||
},
|
},
|
||||||
@ -325,7 +338,7 @@ const useRowStyles = makeStyles({
|
|||||||
marginLeft: 3,
|
marginLeft: 3,
|
||||||
marginRight: 3,
|
marginRight: 3,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
task: ScheduledTaskExtended;
|
task: ScheduledTaskExtended;
|
||||||
@ -343,15 +356,23 @@ interface RowProps {
|
|||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
const { task } = props;
|
const { task } = props;
|
||||||
const classes = useRowStyles();
|
const classes = useRowStyles();
|
||||||
|
const history = useHistory();
|
||||||
return (
|
return (
|
||||||
<TableRow key={task.id} selected={props.isSelected}>
|
<TableRow
|
||||||
<TableCell padding="checkbox">
|
key={task.id}
|
||||||
<Checkbox
|
className={classes.root}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
selected={props.isSelected}
|
||||||
props.onSelectChange(event.target.checked)
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
}
|
>
|
||||||
checked={props.isSelected}
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
/>
|
<IconButton>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
props.onSelectChange(event.target.checked)
|
||||||
|
}
|
||||||
|
checked={props.isSelected}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell component="th" scope="row">
|
||||||
{uuidPrefix(task.id)}
|
{uuidPrefix(task.id)}
|
||||||
@ -371,6 +392,7 @@ function Row(props: RowProps) {
|
|||||||
className={classes.actionCell}
|
className={classes.actionCell}
|
||||||
onMouseEnter={props.onActionCellEnter}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{props.showActions ? (
|
{props.showActions ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { connect, ConnectedProps } from "react-redux";
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
import Paper from "@material-ui/core/Paper";
|
import Paper from "@material-ui/core/Paper";
|
||||||
import Chip from "@material-ui/core/Chip";
|
import Chip from "@material-ui/core/Chip";
|
||||||
|
import InputBase from "@material-ui/core/InputBase";
|
||||||
|
import SearchIcon from "@material-ui/icons/Search";
|
||||||
import ActiveTasksTable from "./ActiveTasksTable";
|
import ActiveTasksTable from "./ActiveTasksTable";
|
||||||
import PendingTasksTable from "./PendingTasksTable";
|
import PendingTasksTable from "./PendingTasksTable";
|
||||||
import ScheduledTasksTable from "./ScheduledTasksTable";
|
import ScheduledTasksTable from "./ScheduledTasksTable";
|
||||||
import RetryTasksTable from "./RetryTasksTable";
|
import RetryTasksTable from "./RetryTasksTable";
|
||||||
import ArchivedTasksTable from "./ArchivedTasksTable";
|
import ArchivedTasksTable from "./ArchivedTasksTable";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { queueDetailsPath } from "../paths";
|
import { queueDetailsPath, taskDetailsPath } from "../paths";
|
||||||
import { QueueInfo } from "../reducers/queuesReducer";
|
import { QueueInfo } from "../reducers/queuesReducer";
|
||||||
import { AppState } from "../store";
|
import { AppState } from "../store";
|
||||||
import { isDarkTheme } from "../theme";
|
import { isDarkTheme } from "../theme";
|
||||||
@ -101,6 +103,38 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
marginLeft: "2px",
|
marginLeft: "2px",
|
||||||
},
|
},
|
||||||
|
searchbar: {
|
||||||
|
marginLeft: theme.spacing(4),
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
position: "relative",
|
||||||
|
width: "312px",
|
||||||
|
borderRadius: "18px",
|
||||||
|
backgroundColor: isDarkTheme(theme) ? "#303030" : theme.palette.grey[100],
|
||||||
|
"&:hover, &:focus": {
|
||||||
|
backgroundColor: isDarkTheme(theme) ? "#303030" : theme.palette.grey[200],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
searchIcon: {
|
||||||
|
padding: theme.spacing(0, 2),
|
||||||
|
height: "100%",
|
||||||
|
position: "absolute",
|
||||||
|
pointerEvents: "none",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
inputRoot: {
|
||||||
|
color: "inherit",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
inputInput: {
|
||||||
|
padding: theme.spacing(1, 1, 1, 0),
|
||||||
|
// vertical padding + font size from searchIcon
|
||||||
|
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
|
||||||
|
width: "100%",
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function TasksTable(props: Props & ReduxProps) {
|
function TasksTable(props: Props & ReduxProps) {
|
||||||
@ -115,6 +149,8 @@ function TasksTable(props: Props & ReduxProps) {
|
|||||||
{ key: "archived", label: "Archived", count: currentStats.archived },
|
{ key: "archived", label: "Archived", count: currentStats.archived },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper variant="outlined" className={classes.container}>
|
<Paper variant="outlined" className={classes.container}>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
@ -137,6 +173,34 @@ function TasksTable(props: Props & ReduxProps) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={classes.searchbar}>
|
||||||
|
<div className={classes.search}>
|
||||||
|
<div className={classes.searchIcon}>
|
||||||
|
<SearchIcon />
|
||||||
|
</div>
|
||||||
|
<InputBase
|
||||||
|
placeholder="Search by ID"
|
||||||
|
classes={{
|
||||||
|
root: classes.inputRoot,
|
||||||
|
input: classes.inputInput,
|
||||||
|
}}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchQuery(e.target.value);
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
"aria-label": "search",
|
||||||
|
onKeyDown: (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
history.push(
|
||||||
|
taskDetailsPath(props.queue, searchQuery.trim())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TabPanel value="active" selected={props.selected}>
|
<TabPanel value="active" selected={props.selected}>
|
||||||
<ActiveTasksTable queue={props.queue} />
|
<ActiveTasksTable queue={props.queue} />
|
||||||
|
@ -5,8 +5,13 @@ export const paths = {
|
|||||||
SCHEDULERS: "/schedulers",
|
SCHEDULERS: "/schedulers",
|
||||||
QUEUE_DETAILS: "/queues/:qname",
|
QUEUE_DETAILS: "/queues/:qname",
|
||||||
REDIS: "/redis",
|
REDIS: "/redis",
|
||||||
|
TASK_DETAILS: "/queues/:qname/tasks/:taskId",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**************************************************************
|
||||||
|
Path Helper functions
|
||||||
|
**************************************************************/
|
||||||
|
|
||||||
export function queueDetailsPath(qname: string, taskStatus?: string): string {
|
export function queueDetailsPath(qname: string, taskStatus?: string): string {
|
||||||
const path = paths.QUEUE_DETAILS.replace(":qname", qname);
|
const path = paths.QUEUE_DETAILS.replace(":qname", qname);
|
||||||
if (taskStatus) {
|
if (taskStatus) {
|
||||||
@ -14,3 +19,20 @@ export function queueDetailsPath(qname: string, taskStatus?: string): string {
|
|||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function taskDetailsPath(qname: string, taskId: string): string {
|
||||||
|
return paths.TASK_DETAILS.replace(":qname", qname).replace(":taskId", taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**************************************************************
|
||||||
|
URL Params
|
||||||
|
**************************************************************/
|
||||||
|
|
||||||
|
export interface QueueDetailsRouteParams {
|
||||||
|
qname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskDetailsRouteParams {
|
||||||
|
qname: string;
|
||||||
|
taskId: string;
|
||||||
|
}
|
@ -114,6 +114,9 @@ import {
|
|||||||
BATCH_DELETE_PENDING_TASKS_SUCCESS,
|
BATCH_DELETE_PENDING_TASKS_SUCCESS,
|
||||||
BATCH_ARCHIVE_PENDING_TASKS_ERROR,
|
BATCH_ARCHIVE_PENDING_TASKS_ERROR,
|
||||||
BATCH_DELETE_PENDING_TASKS_ERROR,
|
BATCH_DELETE_PENDING_TASKS_ERROR,
|
||||||
|
GET_TASK_INFO_BEGIN,
|
||||||
|
GET_TASK_INFO_ERROR,
|
||||||
|
GET_TASK_INFO_SUCCESS,
|
||||||
} from "../actions/tasksActions";
|
} from "../actions/tasksActions";
|
||||||
import {
|
import {
|
||||||
ActiveTask,
|
ActiveTask,
|
||||||
@ -121,6 +124,7 @@ import {
|
|||||||
PendingTask,
|
PendingTask,
|
||||||
RetryTask,
|
RetryTask,
|
||||||
ScheduledTask,
|
ScheduledTask,
|
||||||
|
TaskInfo,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
|
|
||||||
export interface ActiveTaskExtended extends ActiveTask {
|
export interface ActiveTaskExtended extends ActiveTask {
|
||||||
@ -193,6 +197,11 @@ interface TasksState {
|
|||||||
error: string;
|
error: string;
|
||||||
data: ArchivedTaskExtended[];
|
data: ArchivedTaskExtended[];
|
||||||
};
|
};
|
||||||
|
taskInfo: {
|
||||||
|
loading: boolean;
|
||||||
|
error: string;
|
||||||
|
data?: TaskInfo;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: TasksState = {
|
const initialState: TasksState = {
|
||||||
@ -231,6 +240,10 @@ const initialState: TasksState = {
|
|||||||
error: "",
|
error: "",
|
||||||
data: [],
|
data: [],
|
||||||
},
|
},
|
||||||
|
taskInfo: {
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function tasksReducer(
|
function tasksReducer(
|
||||||
@ -238,6 +251,34 @@ function tasksReducer(
|
|||||||
action: TasksActionTypes
|
action: TasksActionTypes
|
||||||
): TasksState {
|
): TasksState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case GET_TASK_INFO_BEGIN:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
taskInfo: {
|
||||||
|
...state.taskInfo,
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
case GET_TASK_INFO_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
taskInfo: {
|
||||||
|
loading: false,
|
||||||
|
error: action.error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case GET_TASK_INFO_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
taskInfo: {
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
data: action.payload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
case LIST_ACTIVE_TASKS_BEGIN:
|
case LIST_ACTIVE_TASKS_BEGIN:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
251
ui/src/views/TaskDetailsView.tsx
Normal file
251
ui/src/views/TaskDetailsView.tsx
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import React, { useMemo, useEffect } from "react";
|
||||||
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Container from "@material-ui/core/Container";
|
||||||
|
import Grid from "@material-ui/core/Grid";
|
||||||
|
import Paper from "@material-ui/core/Paper";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import Button from "@material-ui/core/Button";
|
||||||
|
import Alert from "@material-ui/lab/Alert";
|
||||||
|
import AlertTitle from "@material-ui/lab/AlertTitle";
|
||||||
|
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import QueueBreadCrumb from "../components/QueueBreadcrumb";
|
||||||
|
import { AppState } from "../store";
|
||||||
|
import { getTaskInfoAsync } from "../actions/tasksActions";
|
||||||
|
import { TaskDetailsRouteParams } from "../paths";
|
||||||
|
import { usePolling } from "../hooks";
|
||||||
|
import { listQueuesAsync } from "../actions/queuesActions";
|
||||||
|
import SyntaxHighlighter from "../components/SyntaxHighlighter";
|
||||||
|
|
||||||
|
function mapStateToProps(state: AppState) {
|
||||||
|
return {
|
||||||
|
loading: state.tasks.taskInfo.loading,
|
||||||
|
error: state.tasks.taskInfo.error,
|
||||||
|
taskInfo: state.tasks.taskInfo.data,
|
||||||
|
pollInterval: state.settings.pollInterval,
|
||||||
|
queues: state.queues.data.map((q) => q.name), // FIXME: This data may not be available
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const connector = connect(mapStateToProps, {
|
||||||
|
getTaskInfoAsync,
|
||||||
|
listQueuesAsync,
|
||||||
|
});
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
container: {
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
},
|
||||||
|
alert: {
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
},
|
||||||
|
breadcrumbs: {
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
|
infoRow: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
infoKeyCell: {
|
||||||
|
width: "140px",
|
||||||
|
},
|
||||||
|
infoValueCell: {
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
paddingTop: theme.spacing(3),
|
||||||
|
paddingBottom: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Props = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
|
function TaskDetailsView(props: Props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { qname, taskId } = useParams<TaskDetailsRouteParams>();
|
||||||
|
const { getTaskInfoAsync, pollInterval, listQueuesAsync, taskInfo } = props;
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const fetchTaskInfo = useMemo(() => {
|
||||||
|
return () => {
|
||||||
|
getTaskInfoAsync(qname, taskId);
|
||||||
|
};
|
||||||
|
}, [qname, taskId, getTaskInfoAsync]);
|
||||||
|
|
||||||
|
usePolling(fetchTaskInfo, pollInterval);
|
||||||
|
|
||||||
|
// Fetch queues data to populate props.queues
|
||||||
|
useEffect(() => {
|
||||||
|
listQueuesAsync();
|
||||||
|
}, [listQueuesAsync]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" className={classes.container}>
|
||||||
|
<Grid container spacing={0}>
|
||||||
|
<Grid item xs={12} className={classes.breadcrumbs}>
|
||||||
|
<QueueBreadCrumb
|
||||||
|
queues={props.queues}
|
||||||
|
queueName={qname}
|
||||||
|
taskId={taskId}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
{props.error ? (
|
||||||
|
<Alert severity="error" className={classes.alert}>
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
{props.error}
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Paper className={classes.paper} variant="outlined">
|
||||||
|
<Typography variant="h6">Task Info</Typography>
|
||||||
|
<div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
ID:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={classes.infoValueCell}>
|
||||||
|
{taskInfo?.id}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
Type:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={classes.infoValueCell}>
|
||||||
|
{taskInfo?.type}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
State:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={classes.infoValueCell}>
|
||||||
|
{taskInfo?.state}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
Queue:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={classes.infoValueCell}>
|
||||||
|
{taskInfo?.queue}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
Retry:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={classes.infoValueCell}>
|
||||||
|
{taskInfo?.retried}/{taskInfo?.max_retry}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
Last Failure:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={classes.infoValueCell}>
|
||||||
|
{taskInfo?.last_failed_at ? (
|
||||||
|
<Typography>
|
||||||
|
{taskInfo?.error_message} ({taskInfo?.last_failed_at})
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography>n/a</Typography>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
Next Process Time:{" "}
|
||||||
|
</Typography>
|
||||||
|
{taskInfo?.next_process_at ? (
|
||||||
|
<Typography>{taskInfo?.next_process_at}</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography>n/a</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography variant="subtitle2" className={classes.infoKeyCell}>
|
||||||
|
Timeout:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={classes.infoValueCell}>
|
||||||
|
{taskInfo?.timeout_seconds ? (
|
||||||
|
<Typography>{taskInfo?.timeout_seconds} seconds</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography>n/a</Typography>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography variant="subtitle2" className={classes.infoKeyCell}>
|
||||||
|
Deadline:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={classes.infoValueCell}>
|
||||||
|
{taskInfo?.deadline ? (
|
||||||
|
<Typography>{taskInfo?.deadline}</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography>n/a</Typography>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography variant="subtitle2" className={classes.infoKeyCell}>
|
||||||
|
Payload:{" "}
|
||||||
|
</Typography>
|
||||||
|
<div className={classes.infoValueCell}>
|
||||||
|
{taskInfo?.payload && (
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language="json"
|
||||||
|
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||||
|
>
|
||||||
|
{taskInfo.payload}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
<div className={classes.footer}>
|
||||||
|
<Button
|
||||||
|
startIcon={<ArrowBackIcon />}
|
||||||
|
onClick={() => history.goBack()}
|
||||||
|
>
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connector(TaskDetailsView);
|
@ -9,6 +9,7 @@ import QueueBreadCrumb from "../components/QueueBreadcrumb";
|
|||||||
import { useParams, useLocation } from "react-router-dom";
|
import { useParams, useLocation } from "react-router-dom";
|
||||||
import { listQueuesAsync } from "../actions/queuesActions";
|
import { listQueuesAsync } from "../actions/queuesActions";
|
||||||
import { AppState } from "../store";
|
import { AppState } from "../store";
|
||||||
|
import { QueueDetailsRouteParams } from "../paths";
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
@ -37,16 +38,12 @@ function useQuery(): URLSearchParams {
|
|||||||
return new URLSearchParams(useLocation().search);
|
return new URLSearchParams(useLocation().search);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RouteParams {
|
|
||||||
qname: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validStatus = ["active", "pending", "scheduled", "retry", "archived"];
|
const validStatus = ["active", "pending", "scheduled", "retry", "archived"];
|
||||||
const defaultStatus = "active";
|
const defaultStatus = "active";
|
||||||
|
|
||||||
function TasksView(props: ConnectedProps<typeof connector>) {
|
function TasksView(props: ConnectedProps<typeof connector>) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { qname } = useParams<RouteParams>();
|
const { qname } = useParams<QueueDetailsRouteParams>();
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
let selected = query.get("status");
|
let selected = query.get("status");
|
||||||
if (!selected || !validStatus.includes(selected)) {
|
if (!selected || !validStatus.includes(selected)) {
|
||||||
@ -62,7 +59,7 @@ function TasksView(props: ConnectedProps<typeof connector>) {
|
|||||||
<Container maxWidth="lg">
|
<Container maxWidth="lg">
|
||||||
<Grid container spacing={0} className={classes.container}>
|
<Grid container spacing={0} className={classes.container}>
|
||||||
<Grid item xs={12} className={classes.breadcrumbs}>
|
<Grid item xs={12} className={classes.breadcrumbs}>
|
||||||
<QueueBreadCrumb queues={props.queues} selectedQueue={qname} />
|
<QueueBreadCrumb queues={props.queues} queueName={qname} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} className={classes.banner}>
|
<Grid item xs={12} className={classes.banner}>
|
||||||
<QueueInfoBanner qname={qname} />
|
<QueueInfoBanner qname={qname} />
|
||||||
|
Loading…
Reference in New Issue
Block a user