villebro commented on code in PR #36368: URL: https://github.com/apache/superset/pull/36368#discussion_r2738525769
########## superset-frontend/src/pages/TaskList/index.tsx: ########## @@ -0,0 +1,610 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SupersetClient } from '@superset-ui/core'; +import { t, useTheme } from '@apache-superset/core'; +import { useMemo, useCallback, useState } from 'react'; +import { Tooltip, Label, Modal, Checkbox } from '@superset-ui/core/components'; +import { + CreatedInfo, + ListView, + ListViewFilterOperator as FilterOperator, + type ListViewFilters, + FacePile, +} from 'src/components'; +import { Icons } from '@superset-ui/core/components/Icons'; +import withToasts from 'src/components/MessageToasts/withToasts'; +import SubMenu from 'src/features/home/SubMenu'; +import { useListViewResource } from 'src/views/CRUD/hooks'; +import { createErrorHandler, createFetchRelated } from 'src/views/CRUD/utils'; +import TaskStatusIcon from 'src/features/tasks/TaskStatusIcon'; +import TaskPayloadPopover from 'src/features/tasks/TaskPayloadPopover'; +import TaskStackTracePopover from 'src/features/tasks/TaskStackTracePopover'; +import { formatDuration } from 'src/features/tasks/timeUtils'; +import { + Task, + TaskStatus, + TaskScope, + canAbortTask, + isTaskAborting, +} from 'src/features/tasks/types'; +import { isUserAdmin } from 'src/dashboard/util/permissionUtils'; +import getBootstrapData from 'src/utils/getBootstrapData'; + +const PAGE_SIZE = 25; + +interface TaskListProps { + addDangerToast: (msg: string) => void; + addSuccessToast: (msg: string) => void; + user: { + userId: string | number; + firstName: string; + lastName: string; + }; +} + +function TaskList({ addDangerToast, addSuccessToast, user }: TaskListProps) { + const theme = useTheme(); + const { + state: { loading, resourceCount: tasksCount, resourceCollection: tasks }, + fetchData, + refreshData, + } = useListViewResource<Task>('task', t('task'), addDangerToast); + + // Get full user with roles to check admin status + const bootstrapData = getBootstrapData(); + const fullUser = bootstrapData?.user; + const isAdmin = useMemo(() => isUserAdmin(fullUser), [fullUser]); + + // State for cancel confirmation modal + const [cancelModalTask, setCancelModalTask] = useState<Task | null>(null); + const [forceCancel, setForceCancel] = useState(false); + + // Determine dialog message based on task context + const getCancelDialogMessage = useCallback((task: Task) => { + const isSharedTask = task.scope === TaskScope.Shared; + const subscriberCount = task.subscriber_count || 0; + const otherSubscribers = subscriberCount - 1; + + // If it's going to abort (private, system, or last subscriber) + if (!isSharedTask || subscriberCount <= 1) { + return t('This will cancel the task.'); + } + + // Shared task with multiple subscribers + return t( + "You'll be removed from this task. It will continue running for %s other subscriber(s).", + otherSubscribers, + ); + }, []); + + // Get force abort message for admin checkbox + const getForceAbortMessage = useCallback((task: Task) => { + const subscriberCount = task.subscriber_count || 0; + return t( + 'This will abort (stop) the task for all %s subscriber(s).', + subscriberCount, + ); + }, []); + + // Check if current user is subscribed to a task + const isUserSubscribed = useCallback( + (task: Task) => + task.subscribers?.some((sub: any) => sub.user_id === user.userId) ?? + false, + [user.userId], + ); + + // Check if force cancel option should be shown (for admins on shared tasks) + const showForceCancelOption = useCallback( + (task: Task) => { + const isSharedTask = task.scope === TaskScope.Shared; + const subscriberCount = task.subscriber_count || 0; + const userSubscribed = isUserSubscribed(task); + // Show for admins on shared tasks when: + // - Not subscribed (can only abort, so show checkbox pre-checked disabled), OR + // - Multiple subscribers (can choose between unsubscribe and force abort) + // Don't show when admin is the sole subscriber - cancel will abort anyway + return ( + isAdmin && isSharedTask && (subscriberCount > 1 || !userSubscribed) + ); + }, + [isAdmin, isUserSubscribed], + ); + + // Check if force cancel checkbox should be disabled (admin not subscribed) + const isForceCancelDisabled = useCallback( + (task: Task) => isAdmin && !isUserSubscribed(task), + [isAdmin, isUserSubscribed], + ); + + const handleTaskCancel = useCallback( + (task: Task, force: boolean = false) => { + SupersetClient.post({ + endpoint: `/api/v1/task/${task.uuid}/cancel`, + jsonPayload: force ? { force: true } : {}, + }).then( + ({ json }) => { + refreshData(); + const { action } = json as { action: string }; + if (action === 'aborted') { + addSuccessToast( + t('Task cancelled: %s', task.task_name || task.task_key), + ); + } else { + addSuccessToast( + t( + 'You have been removed from task: %s', + task.task_name || task.task_key, + ), + ); + } + }, + createErrorHandler(errMsg => + addDangerToast( + t('There was an issue cancelling the task: %s', errMsg), + ), + ), + ); + }, + [addDangerToast, addSuccessToast, refreshData], + ); + + // Handle opening the cancel modal - set initial forceCancel state + const openCancelModal = useCallback( + (task: Task) => { + // Pre-check force cancel if admin is not subscribed + const shouldPreCheck = isAdmin && !isUserSubscribed(task); + setForceCancel(shouldPreCheck); + setCancelModalTask(task); + }, + [isAdmin, isUserSubscribed], + ); + + // Handle modal confirmation + const handleCancelConfirm = useCallback(() => { + if (cancelModalTask) { + handleTaskCancel(cancelModalTask, forceCancel); + setCancelModalTask(null); + setForceCancel(false); + } + }, [cancelModalTask, forceCancel, handleTaskCancel]); + + // Handle modal close + const handleCancelModalClose = useCallback(() => { + setCancelModalTask(null); + setForceCancel(false); + }, []); + + const columns = useMemo( + () => [ + { + Cell: ({ + row: { + original: { task_name, task_key, uuid }, + }, + }: any) => { Review Comment: Thanks, that actually caught a bug! -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
