From 62398d04b466ee75d43bd353236deff20a859049 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 8 Jan 2020 13:03:36 -0800 Subject: [PATCH] refactor(ui): normalization of tasks (#16445) * refactor: normalize tasks resource * fix: pull tasks off of response * refactor: use action to clear current task * refactor: stop getting all tasks in every thunk --- ui/src/cloud/actions/limits.ts | 5 +- .../components/SaveAsTaskForm.tsx | 21 +- ui/src/schemas/index.ts | 34 +- ui/src/shared/components/GetResources.tsx | 2 +- ui/src/shared/selectors/getResourcesStatus.ts | 1 + ui/src/store/configureStore.ts | 4 +- ui/src/tasks/actions/creators.ts | 143 ++++ ui/src/tasks/actions/index.ts | 649 ------------------ ui/src/tasks/actions/thunks.ts | 497 ++++++++++++++ ui/src/tasks/components/TaskCard.test.tsx | 2 +- ui/src/tasks/components/TaskCard.tsx | 28 +- ui/src/tasks/components/TaskExportOverlay.tsx | 2 +- ui/src/tasks/components/TaskForm.tsx | 3 +- ui/src/tasks/components/TaskHeader.tsx | 19 +- .../TaskImportFromTemplateOverlay.tsx | 2 +- ui/src/tasks/components/TaskImportOverlay.tsx | 2 +- ui/src/tasks/components/TaskRunsPage.tsx | 11 +- ui/src/tasks/components/TaskRunsRow.tsx | 7 +- .../components/TaskScheduleFormField.tsx | 2 +- ui/src/tasks/components/TasksList.tsx | 12 +- ui/src/tasks/containers/TaskEditPage.tsx | 25 +- ui/src/tasks/containers/TaskPage.tsx | 26 +- ui/src/tasks/containers/TasksPage.tsx | 44 +- ui/src/tasks/reducers/helpers.ts | 32 + ui/src/tasks/reducers/index.ts | 257 ++++--- ui/src/tasks/reducers/tasks.test.ts | 34 +- ui/src/templates/actions/index.ts | 2 +- ui/src/templates/api/index.ts | 61 +- ui/src/types/resources.ts | 23 +- ui/src/types/schemas.ts | 21 +- ui/src/types/stores.ts | 2 - ui/src/types/tasks.ts | 32 +- ui/src/utils/api.ts | 20 +- ui/src/utils/taskOptionsToFluxScript.ts | 25 +- 34 files changed, 1084 insertions(+), 966 deletions(-) create mode 100644 ui/src/tasks/actions/creators.ts delete mode 100644 ui/src/tasks/actions/index.ts create mode 100644 ui/src/tasks/actions/thunks.ts create mode 100644 ui/src/tasks/reducers/helpers.ts diff --git a/ui/src/cloud/actions/limits.ts b/ui/src/cloud/actions/limits.ts index d1978afa97..56a453eede 100644 --- a/ui/src/cloud/actions/limits.ts +++ b/ui/src/cloud/actions/limits.ts @@ -304,12 +304,11 @@ export const checkBucketLimits = () => (dispatch, getState: GetState) => { export const checkTaskLimits = () => (dispatch, getState: GetState) => { try { const { - tasks: {list}, cloud: {limits}, + resources, } = getState() - const tasksMax = extractTaskMax(limits) - const tasksCount = list.length + const tasksCount = resources.tasks.allIDs.length if (tasksCount >= tasksMax) { dispatch(setTaskLimitStatus(LimitStatus.EXCEEDED)) diff --git a/ui/src/dataExplorer/components/SaveAsTaskForm.tsx b/ui/src/dataExplorer/components/SaveAsTaskForm.tsx index ed367d9329..41783ca388 100644 --- a/ui/src/dataExplorer/components/SaveAsTaskForm.tsx +++ b/ui/src/dataExplorer/components/SaveAsTaskForm.tsx @@ -7,16 +7,15 @@ import {withRouter, WithRouterProps} from 'react-router' import TaskForm from 'src/tasks/components/TaskForm' // Actions +import {saveNewScript} from 'src/tasks/actions/thunks' import { - saveNewScript, setTaskOption, clearTask, setNewScript, -} from 'src/tasks/actions' +} from 'src/tasks/actions/creators' import {refreshTimeMachineVariableValues} from 'src/timeMachine/actions/queries' // Utils -import {getActiveTimeMachine, getActiveQuery} from 'src/timeMachine/selectors' import {getTimeRangeVars} from 'src/variables/utils/getTimeRangeVars' import {getWindowVars} from 'src/variables/utils/getWindowVars' import {formatVarsOption} from 'src/variables/utils/formatVarsOption' @@ -24,17 +23,20 @@ import { taskOptionsToFluxScript, addDestinationToFluxScript, } from 'src/utils/taskOptionsToFluxScript' +import {getVariableAssignments} from 'src/variables/selectors' import {getOrg} from 'src/organizations/selectors' +import {getActiveTimeMachine, getActiveQuery} from 'src/timeMachine/selectors' // Types -import {AppState, TimeRange, VariableAssignment} from 'src/types' import { + AppState, + TimeRange, + VariableAssignment, TaskSchedule, TaskOptions, TaskOptionKeys, -} from 'src/utils/taskOptionsToFluxScript' -import {DashboardDraftQuery} from 'src/types/dashboards' -import {getVariableAssignments} from 'src/variables/selectors' + DashboardDraftQuery, +} from 'src/types' interface OwnProps { dismiss: () => void @@ -163,10 +165,7 @@ class SaveAsTaskForm extends PureComponent { } const mstp = (state: AppState): StateProps => { - const { - tasks: {newScript, taskOptions}, - } = state - + const {newScript, taskOptions} = state.resources.tasks const {timeRange} = getActiveTimeMachine(state) const activeQuery = getActiveQuery(state) const org = getOrg(state) diff --git a/ui/src/schemas/index.ts b/ui/src/schemas/index.ts index 134988659c..d829109709 100644 --- a/ui/src/schemas/index.ts +++ b/ui/src/schemas/index.ts @@ -2,35 +2,50 @@ import {schema} from 'normalizr' // Types -import {ResourceType, Telegraf} from 'src/types' +import {ResourceType, Telegraf, Task, Label} from 'src/types' + +// Utils +import {addLabelDefaults} from 'src/labels/utils' /* Authorizations */ -// Defines the schema for the "auth" resource +// Defines the schema for the "authorizations" resource export const auth = new schema.Entity(ResourceType.Authorizations) export const arrayOfAuths = [auth] /* Buckets */ -// Defines the schema for the "bucket" resource +// Defines the schema for the "buckets" resource export const bucket = new schema.Entity(ResourceType.Buckets) export const arrayOfBuckets = [bucket] /* Members */ -// Defines the schema for the "member" resource +// Defines the schema for the "members" resource export const member = new schema.Entity(ResourceType.Members) export const arrayOfMembers = [member] /* Organizations */ -// Defines the schema for the "member" resource +// Defines the schema for the "organizations" resource export const org = new schema.Entity(ResourceType.Orgs) export const arrayOfOrgs = [org] +/* Tasks */ +// Defines the schema for the tasks resource +export const task = new schema.Entity( + ResourceType.Tasks, + {}, + { + processStrategy: (task: Task) => addLabels(task), + } +) + +export const arrayOfTasks = [task] + /* Telegrafs */ -// Defines the schema for the "member" resource +// Defines the schema for the "telegrafs" resource export const telegraf = new schema.Entity( ResourceType.Telegrafs, {}, @@ -69,3 +84,10 @@ export const arrayOfTelegrafs = [telegraf] export const scraper = new schema.Entity(ResourceType.Scrapers) export const arrayOfScrapers = [scraper] + +export const addLabels = (resource: R): R => { + return { + ...resource, + labels: (resource.labels || []).map(addLabelDefaults), + } +} diff --git a/ui/src/shared/components/GetResources.tsx b/ui/src/shared/components/GetResources.tsx index 5c8948592b..ac8fc6e996 100644 --- a/ui/src/shared/components/GetResources.tsx +++ b/ui/src/shared/components/GetResources.tsx @@ -10,7 +10,7 @@ import {getPlugins} from 'src/dataLoaders/actions/telegrafEditor' import {getVariables} from 'src/variables/actions' import {getScrapers} from 'src/scrapers/actions/thunks' import {getDashboardsAsync} from 'src/dashboards/actions' -import {getTasks} from 'src/tasks/actions' +import {getTasks} from 'src/tasks/actions/thunks' import {getAuthorizations} from 'src/authorizations/actions/thunks' import {getTemplates} from 'src/templates/actions' import {getMembers} from 'src/members/actions/thunks' diff --git a/ui/src/shared/selectors/getResourcesStatus.ts b/ui/src/shared/selectors/getResourcesStatus.ts index 361250d558..120180b827 100644 --- a/ui/src/shared/selectors/getResourcesStatus.ts +++ b/ui/src/shared/selectors/getResourcesStatus.ts @@ -11,6 +11,7 @@ export const getResourcesStatus = ( case ResourceType.Members: case ResourceType.Buckets: case ResourceType.Telegrafs: + case ResourceType.Tasks: case ResourceType.Scrapers: case ResourceType.Authorizations: { return state.resources[resource].status diff --git a/ui/src/store/configureStore.ts b/ui/src/store/configureStore.ts index 5a5883d447..a002b61bb2 100644 --- a/ui/src/store/configureStore.ts +++ b/ui/src/store/configureStore.ts @@ -71,12 +71,12 @@ export const rootReducer = combineReducers({ members: membersReducer, orgs: orgsReducer, scrapers: scrapersReducer, - tokens: authsReducer, + tasks: tasksReducer, telegrafs: telegrafsReducer, + tokens: authsReducer, }), routing: routerReducer, rules: rulesReducer, - tasks: tasksReducer, telegrafEditor: editorReducer, telegrafEditorActivePlugins: activePluginsReducer, telegrafEditorPlugins: pluginsReducer, diff --git a/ui/src/tasks/actions/creators.ts b/ui/src/tasks/actions/creators.ts new file mode 100644 index 0000000000..cd659947c5 --- /dev/null +++ b/ui/src/tasks/actions/creators.ts @@ -0,0 +1,143 @@ +// Types +import { + Run, + LogEvent, + TaskOptionKeys, + RemoteDataState, + TaskEntities, +} from 'src/types' +import {NormalizedSchema} from 'normalizr' + +export const SET_TASKS = 'SET_TASKS' +export const EDIT_TASK = 'EDIT_TASK' +export const SET_TASK_OPTION = 'SET_TASK_OPTION' +export const SET_ALL_TASK_OPTIONS = 'SET_ALL_TASK_OPTIONS' +export const CLEAR_TASK = 'CLEAR_TASK' +export const CLEAR_CURRENT_TASK = 'CLEAR_CURRENT_TASK' +export const SET_NEW_SCRIPT = 'SET_NEW_SCRIPT' +export const SET_CURRENT_SCRIPT = 'SET_CURRENT_SCRIPT' +export const SET_CURRENT_TASK = 'SET_CURRENT_TASK' +export const SET_SEARCH_TERM = 'SET_SEARCH_TERM' +export const SET_SHOW_INACTIVE = 'SET_SHOW_INACTIVE' +export const SET_RUNS = 'SET_RUNS' +export const SET_LOGS = 'SET_LOGS' +export const REMOVE_TASK = 'REMOVE_TASK' +export const ADD_TASK = 'ADD_TASK' + +export type Action = + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + +// R is the type of the value of the "result" key in normalizr's normalization +type TasksSchema = NormalizedSchema< + TaskEntities, + R +> +export const setTasks = ( + status: RemoteDataState, + schema?: TasksSchema +) => + ({ + type: SET_TASKS, + status, + schema, + } as const) + +export const addTask = (schema: TasksSchema) => + ({ + type: ADD_TASK, + schema, + } as const) + +export const editTask = (schema: TasksSchema) => + ({ + type: EDIT_TASK, + schema, + } as const) + +export const removeTask = (id: string) => + ({ + type: REMOVE_TASK, + id, + } as const) + +export const setCurrentTask = (schema: TasksSchema) => + ({ + type: SET_CURRENT_TASK, + schema, + } as const) + +export const clearCurrentTask = () => + ({ + type: CLEAR_CURRENT_TASK, + } as const) + +export const setTaskOption = (taskOption: { + key: TaskOptionKeys + value: string +}) => + ({ + type: SET_TASK_OPTION, + ...taskOption, + } as const) + +export const setAllTaskOptions = (schema: TasksSchema) => + ({ + type: SET_ALL_TASK_OPTIONS, + schema, + } as const) + +export const clearTask = () => + ({ + type: CLEAR_TASK, + } as const) + +export const setNewScript = (script: string) => + ({ + type: SET_NEW_SCRIPT, + script, + } as const) + +export const setCurrentScript = (script: string) => + ({ + type: SET_CURRENT_SCRIPT, + script, + } as const) + +export const setSearchTerm = (searchTerm: string) => + ({ + type: SET_SEARCH_TERM, + searchTerm, + } as const) + +export const setShowInactive = () => + ({ + type: SET_SHOW_INACTIVE, + } as const) + +export const setRuns = (runs: Run[], runStatus: RemoteDataState) => + ({ + type: SET_RUNS, + runs, + runStatus, + } as const) + +export const setLogs = (logs: LogEvent[]) => + ({ + type: SET_LOGS, + logs, + } as const) diff --git a/ui/src/tasks/actions/index.ts b/ui/src/tasks/actions/index.ts deleted file mode 100644 index 181c0d523e..0000000000 --- a/ui/src/tasks/actions/index.ts +++ /dev/null @@ -1,649 +0,0 @@ -// Libraries -import {push, goBack} from 'react-router-redux' -import _ from 'lodash' -// APIs -import {notify} from 'src/shared/actions/notifications' -import { - taskNotCreated, - tasksFetchFailed, - taskDeleteFailed, - taskNotFound, - taskUpdateFailed, - taskUpdateSuccess, - taskCreatedSuccess, - taskDeleteSuccess, - taskCloneSuccess, - taskCloneFailed, - taskRunSuccess, - taskGetFailed, - importTaskFailed, - importTaskSucceeded, -} from 'src/shared/copy/notifications' -import {createTaskFromTemplate as createTaskFromTemplateAJAX} from 'src/templates/api' -import {addLabelDefaults} from 'src/labels/utils' -import { - deleteTask as apiDeleteTask, - deleteTasksLabel as apiDeleteTasksLabel, - getTask as apiGetTask, - getTasks as apiGetTasks, - getTasksRuns as apiGetTasksRuns, - getTasksRunsLogs as apiGetTasksRunsLogs, - patchTask as apiPatchTask, - postTask as apiPostTask, - postTasksLabel as apiPostTasksLabel, - postTasksRun as apiPostTasksRun, -} from 'src/client' -// Actions -import {setExportTemplate} from 'src/templates/actions' - -// Constants -import * as copy from 'src/shared/copy/notifications' - -// Types -import {Label, TaskTemplate, LogEvent, Run, Task, GetState} from 'src/types' -import {RemoteDataState} from '@influxdata/clockface' -import {Task as ITask} from 'src/client' - -// Utils -import {getErrorMessage} from 'src/utils/api' -import {insertPreambleInScript} from 'src/shared/utils/insertPreambleInScript' -import {TaskOptionKeys, TaskSchedule} from 'src/utils/taskOptionsToFluxScript' -import {taskToTemplate} from 'src/shared/utils/resourceToTemplate' -import {isLimitError} from 'src/cloud/utils/limits' -import {checkTaskLimits} from 'src/cloud/actions/limits' -import {getOrg} from 'src/organizations/selectors' - -export type Action = - | SetNewScript - | SetTasks - | SetSearchTerm - | SetCurrentScript - | SetCurrentTask - | SetShowInactive - | SetTaskInterval - | SetTaskCron - | ClearTask - | SetTaskOption - | SetAllTaskOptions - | SetRuns - | SetLogs - | UpdateTask - | SetTaskStatus - -type GetStateFunc = GetState - -export interface SetAllTaskOptions { - type: 'SET_ALL_TASK_OPTIONS' - payload: Task -} - -export interface SetTaskStatus { - type: 'SET_TASKS_STATUS' - payload: { - status: RemoteDataState - } -} - -export interface ClearTask { - type: 'CLEAR_TASK' -} - -export interface SetTaskInterval { - type: 'SET_TASK_INTERVAL' - payload: { - interval: string - } -} - -export interface SetTaskCron { - type: 'SET_TASK_CRON' - payload: { - cron: string - } -} - -export interface SetNewScript { - type: 'SET_NEW_SCRIPT' - payload: { - script: string - } -} -export interface SetCurrentScript { - type: 'SET_CURRENT_SCRIPT' - payload: { - script: string - } -} -export interface SetCurrentTask { - type: 'SET_CURRENT_TASK' - payload: { - task: Task - } -} - -export interface SetTasks { - type: 'SET_TASKS' - payload: { - tasks: Task[] - } -} - -export interface SetSearchTerm { - type: 'SET_SEARCH_TERM' - payload: { - searchTerm: string - } -} - -export interface SetShowInactive { - type: 'SET_SHOW_INACTIVE' - payload: {} -} - -export interface SetTaskOption { - type: 'SET_TASK_OPTION' - payload: { - key: TaskOptionKeys - value: string - } -} - -export interface SetRuns { - type: 'SET_RUNS' - payload: { - runs: Run[] - runStatus: RemoteDataState - } -} - -export interface SetLogs { - type: 'SET_LOGS' - payload: { - logs: LogEvent[] - } -} - -export interface UpdateTask { - type: 'UPDATE_TASK' - payload: { - task: Task - } -} - -export const addDefaults = (task: ITask): Task => { - return { - ...task, - labels: (task.labels || []).map(addLabelDefaults), - } -} - -export const setTaskOption = (taskOption: { - key: TaskOptionKeys - value: string -}): SetTaskOption => ({ - type: 'SET_TASK_OPTION', - payload: taskOption, -}) - -export const setTasksStatus = (status: RemoteDataState): SetTaskStatus => ({ - type: 'SET_TASKS_STATUS', - payload: {status}, -}) - -export const setAllTaskOptions = (task: Task): SetAllTaskOptions => ({ - type: 'SET_ALL_TASK_OPTIONS', - payload: task, -}) - -export const clearTask = (): ClearTask => ({ - type: 'CLEAR_TASK', -}) - -export const setNewScript = (script: string): SetNewScript => ({ - type: 'SET_NEW_SCRIPT', - payload: {script}, -}) - -export const setCurrentScript = (script: string): SetCurrentScript => ({ - type: 'SET_CURRENT_SCRIPT', - payload: {script}, -}) - -export const setCurrentTask = (task: Task): SetCurrentTask => ({ - type: 'SET_CURRENT_TASK', - payload: {task}, -}) - -export const setTasks = (tasks: Task[]): SetTasks => ({ - type: 'SET_TASKS', - payload: {tasks}, -}) - -export const setSearchTerm = (searchTerm: string): SetSearchTerm => ({ - type: 'SET_SEARCH_TERM', - payload: {searchTerm}, -}) - -export const setShowInactive = (): SetShowInactive => ({ - type: 'SET_SHOW_INACTIVE', - payload: {}, -}) - -export const setRuns = (runs: Run[], runStatus: RemoteDataState): SetRuns => ({ - type: 'SET_RUNS', - payload: {runs, runStatus}, -}) - -export const setLogs = (logs: LogEvent[]): SetLogs => ({ - type: 'SET_LOGS', - payload: {logs}, -}) - -export const updateTask = (task: Task): UpdateTask => ({ - type: 'UPDATE_TASK', - payload: {task}, -}) - -// Thunks -export const getTasks = () => async ( - dispatch, - getState: GetStateFunc -): Promise => { - try { - dispatch(setTasksStatus(RemoteDataState.Loading)) - - const org = getOrg(getState()) - const resp = await apiGetTasks({query: {orgID: org.id}}) - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - const tasks = resp.data.tasks.map(task => addDefaults(task)) - - dispatch(setTasks(tasks)) - dispatch(setTasksStatus(RemoteDataState.Done)) - } catch (e) { - dispatch(setTasksStatus(RemoteDataState.Error)) - console.error(e) - const message = getErrorMessage(e) - dispatch(notify(tasksFetchFailed(message))) - } -} - -export const addTaskLabelAsync = (taskID: string, label: Label) => async ( - dispatch -): Promise => { - try { - const postResp = await apiPostTasksLabel({ - taskID, - data: {labelID: label.id}, - }) - - if (postResp.status !== 201) { - throw new Error(postResp.data.message) - } - - const resp = await apiGetTask({taskID}) - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - const task = addDefaults(resp.data) - - dispatch(updateTask(task)) - } catch (error) { - console.error(error) - dispatch(notify(copy.addTaskLabelFailed())) - } -} - -export const removeTaskLabelAsync = (taskID: string, label: Label) => async ( - dispatch -): Promise => { - try { - const deleteResp = await apiDeleteTasksLabel({taskID, labelID: label.id}) - if (deleteResp.status !== 204) { - throw new Error(deleteResp.data.message) - } - const resp = await apiGetTask({taskID}) - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - const task = addDefaults(resp.data) - - dispatch(updateTask(task)) - } catch (error) { - console.error(error) - dispatch(notify(copy.removeTaskLabelFailed())) - } -} - -export const updateTaskStatus = (task: Task) => async dispatch => { - try { - const resp = await apiPatchTask({ - taskID: task.id, - data: {status: task.status}, - }) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - dispatch(getTasks()) - dispatch(notify(taskUpdateSuccess())) - } catch (e) { - console.error(e) - const message = getErrorMessage(e) - dispatch(notify(taskUpdateFailed(message))) - } -} - -export const updateTaskName = ( - name: string, - taskID: string -) => async dispatch => { - try { - const resp = await apiPatchTask({taskID, data: {name}}) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - dispatch(getTasks()) - dispatch(notify(taskUpdateSuccess())) - } catch (e) { - console.error(e) - const message = getErrorMessage(e) - dispatch(notify(taskUpdateFailed(message))) - } -} - -export const deleteTask = (task: Task) => async dispatch => { - try { - const resp = await apiDeleteTask({taskID: task.id}) - - if (resp.status !== 204) { - throw new Error(resp.data.message) - } - - dispatch(getTasks()) - dispatch(notify(taskDeleteSuccess())) - } catch (e) { - console.error(e) - const message = getErrorMessage(e) - dispatch(notify(taskDeleteFailed(message))) - } -} - -export const cloneTask = (task: Task, _) => async dispatch => { - try { - const resp = await apiGetTask({taskID: task.id}) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - const postData = addDefaults(resp.data) - - const newTask = await apiPostTask({data: postData}) - - if (newTask.status !== 201) { - throw new Error(newTask.data.message) - } - - dispatch(notify(taskCloneSuccess(task.name))) - dispatch(getTasks()) - dispatch(checkTaskLimits()) - } catch (error) { - console.error(error) - if (isLimitError(error)) { - dispatch(notify(copy.resourceLimitReached('tasks'))) - } else { - const message = getErrorMessage(error) - dispatch(notify(taskCloneFailed(task.name, message))) - } - } -} - -export const selectTaskByID = (id: string) => async ( - dispatch -): Promise => { - try { - const resp = await apiGetTask({taskID: id}) - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - const task = addDefaults(resp.data) - dispatch(setCurrentTask(task)) - } catch (e) { - console.error(e) - dispatch(goToTasks()) - const message = getErrorMessage(e) - dispatch(notify(taskNotFound(message))) - } -} - -export const setAllTaskOptionsByID = (taskID: string) => async ( - dispatch -): Promise => { - try { - const resp = await apiGetTask({taskID}) - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - const task = addDefaults(resp.data) - dispatch(setAllTaskOptions(task)) - } catch (e) { - console.error(e) - dispatch(goToTasks()) - const message = getErrorMessage(e) - dispatch(notify(taskNotFound(message))) - } -} - -export const selectTask = (task: Task) => ( - dispatch, - getState: GetStateFunc -) => { - const org = getOrg(getState()) - - dispatch(push(`/orgs/${org.id}/tasks/${task.id}`)) -} - -export const goToTasks = () => (dispatch, getState: GetStateFunc) => { - const org = getOrg(getState()) - - dispatch(push(`/orgs/${org.id}/tasks`)) -} - -export const cancel = () => dispatch => { - dispatch(setCurrentTask(null)) - dispatch(goBack()) -} - -export const updateScript = () => async (dispatch, getState: GetStateFunc) => { - try { - const { - tasks: {currentScript: script, currentTask: task, taskOptions}, - } = getState() - - const updatedTask: Partial & { - name: string - flux: string - token: string - } = { - flux: script, - name: taskOptions.name, - offset: taskOptions.offset, - token: null, - } - - if (taskOptions.taskScheduleType === TaskSchedule.interval) { - updatedTask.every = taskOptions.interval - updatedTask.cron = null - } else { - updatedTask.cron = taskOptions.cron - updatedTask.every = null - } - - const resp = await apiPatchTask({taskID: task.id, data: updatedTask}) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - dispatch(goToTasks()) - dispatch(setCurrentTask(null)) - dispatch(notify(taskUpdateSuccess())) - } catch (e) { - console.error(e) - const message = getErrorMessage(e) - dispatch(notify(taskUpdateFailed(message))) - } -} - -export const saveNewScript = (script: string, preamble: string) => async ( - dispatch, - getState: GetStateFunc -): Promise => { - try { - const fluxScript = await insertPreambleInScript(script, preamble) - const org = getOrg(getState()) - const resp = await apiPostTask({data: {orgID: org.id, flux: fluxScript}}) - if (resp.status !== 201) { - throw new Error(resp.data.message) - } - - dispatch(setNewScript('')) - dispatch(clearTask()) - dispatch(getTasks()) - dispatch(goToTasks()) - dispatch(notify(taskCreatedSuccess())) - dispatch(checkTaskLimits()) - } catch (error) { - console.error(error) - if (isLimitError(error)) { - dispatch(notify(copy.resourceLimitReached('tasks'))) - } else { - const message = getErrorMessage(error) - dispatch(notify(taskNotCreated(message))) - } - } -} - -export const getRuns = (taskID: string) => async (dispatch): Promise => { - try { - dispatch(setRuns([], RemoteDataState.Loading)) - dispatch(selectTaskByID(taskID)) - const resp = await apiGetTasksRuns({taskID}) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - const runsWithDuration = resp.data.runs.map(run => { - const finished = new Date(run.finishedAt) - const started = new Date(run.startedAt) - - return { - ...run, - duration: `${runDuration(finished, started)}`, - } - }) - - dispatch(setRuns(runsWithDuration, RemoteDataState.Done)) - } catch (error) { - console.error(error) - const message = getErrorMessage(error) - dispatch(notify(taskGetFailed(message))) - dispatch(setRuns([], RemoteDataState.Error)) - } -} - -export const runTask = (taskID: string) => async dispatch => { - try { - const resp = await apiPostTasksRun({taskID}) - if (resp.status !== 201) { - throw new Error(resp.data.message) - } - dispatch(notify(taskRunSuccess())) - } catch (error) { - const message = getErrorMessage(error) - dispatch(notify(copy.taskRunFailed(message))) - console.error(error) - } -} - -export const getLogs = (taskID: string, runID: string) => async ( - dispatch -): Promise => { - try { - const resp = await apiGetTasksRunsLogs({taskID, runID}) - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - dispatch(setLogs(resp.data.events)) - } catch (error) { - console.error(error) - dispatch(setLogs([])) - } -} - -export const convertToTemplate = (taskID: string) => async ( - dispatch -): Promise => { - try { - dispatch(setExportTemplate(RemoteDataState.Loading)) - const resp = await apiGetTask({taskID}) - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - const task = addDefaults(resp.data) - const taskTemplate = taskToTemplate(task) - - dispatch(setExportTemplate(RemoteDataState.Done, taskTemplate)) - } catch (error) { - dispatch(setExportTemplate(RemoteDataState.Error)) - dispatch(notify(copy.createTemplateFailed(error))) - } -} - -export const createTaskFromTemplate = (template: TaskTemplate) => async ( - dispatch, - getState: GetStateFunc -): Promise => { - try { - const org = getOrg(getState()) - - await createTaskFromTemplateAJAX(template, org.id) - - dispatch(getTasks()) - dispatch(notify(importTaskSucceeded())) - dispatch(checkTaskLimits()) - } catch (error) { - if (isLimitError(error)) { - dispatch(notify(copy.resourceLimitReached('tasks'))) - } else { - dispatch(notify(importTaskFailed(error))) - } - } -} - -export const runDuration = (finishedAt: Date, startedAt: Date): string => { - let timeTag = 'seconds' - - if (isNaN(finishedAt.getTime()) || isNaN(startedAt.getTime())) { - return '' - } - let diff = (finishedAt.getTime() - startedAt.getTime()) / 1000 - - if (diff > 60) { - diff = Math.round(diff / 60) - timeTag = 'minutes' - } - - return diff + ' ' + timeTag -} diff --git a/ui/src/tasks/actions/thunks.ts b/ui/src/tasks/actions/thunks.ts new file mode 100644 index 0000000000..ccf78abd8d --- /dev/null +++ b/ui/src/tasks/actions/thunks.ts @@ -0,0 +1,497 @@ +// Libraries +import {push, goBack} from 'react-router-redux' +import {Dispatch} from 'react' +import {normalize} from 'normalizr' + +// APIs +import * as api from 'src/client' +import {createTaskFromTemplate as createTaskFromTemplateAJAX} from 'src/templates/api' + +// Schemas +import * as schemas from 'src/schemas' + +// Actions +import {setExportTemplate} from 'src/templates/actions' +import {notify, Action as NotifyAction} from 'src/shared/actions/notifications' +import { + addTask, + setTasks, + editTask, + setCurrentTask, + setAllTaskOptions, + setRuns, + setLogs, + clearTask, + removeTask, + setNewScript, + clearCurrentTask, + Action as TaskAction, +} from 'src/tasks/actions/creators' + +// Constants +import * as copy from 'src/shared/copy/notifications' + +// Types +import { + Label, + TaskTemplate, + Task, + GetState, + TaskSchedule, + RemoteDataState, + TaskEntities, +} from 'src/types' + +// Utils +import {getErrorMessage} from 'src/utils/api' +import {insertPreambleInScript} from 'src/shared/utils/insertPreambleInScript' +import {taskToTemplate} from 'src/shared/utils/resourceToTemplate' +import {isLimitError} from 'src/cloud/utils/limits' +import {checkTaskLimits} from 'src/cloud/actions/limits' +import {getOrg} from 'src/organizations/selectors' + +type Action = TaskAction | ExternalActions | ReturnType +type ExternalActions = NotifyAction | ReturnType + +// Thunks +export const getTasks = () => async ( + dispatch: Dispatch, + getState: GetState +): Promise => { + try { + dispatch(setTasks(RemoteDataState.Loading)) + + const org = getOrg(getState()) + const resp = await api.getTasks({query: {orgID: org.id}}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const tasks = normalize( + resp.data.tasks, + schemas.arrayOfTasks + ) + + dispatch(setTasks(RemoteDataState.Done, tasks)) + } catch (error) { + dispatch(setTasks(RemoteDataState.Error)) + const message = getErrorMessage(error) + console.error(error) + dispatch(notify(copy.tasksFetchFailed(message))) + } +} + +export const addTaskLabel = (taskID: string, label: Label) => async ( + dispatch: Dispatch +): Promise => { + try { + const postResp = await api.postTasksLabel({ + taskID, + data: {labelID: label.id}, + }) + + if (postResp.status !== 201) { + throw new Error(postResp.data.message) + } + + const resp = await api.getTask({taskID}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const task = normalize(resp.data, schemas.task) + + dispatch(editTask(task)) + } catch (error) { + console.error(error) + dispatch(notify(copy.addTaskLabelFailed())) + } +} + +export const deleteTaskLabel = (taskID: string, label: Label) => async ( + dispatch: Dispatch +): Promise => { + try { + const deleteResp = await api.deleteTasksLabel({taskID, labelID: label.id}) + if (deleteResp.status !== 204) { + throw new Error(deleteResp.data.message) + } + + const resp = await api.getTask({taskID}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const task = normalize(resp.data, schemas.task) + + dispatch(editTask(task)) + } catch (error) { + console.error(error) + dispatch(notify(copy.removeTaskLabelFailed())) + } +} + +export const updateTaskStatus = (task: Task) => async ( + dispatch: Dispatch +) => { + try { + const resp = await api.patchTask({ + taskID: task.id, + data: {status: task.status}, + }) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const normTask = normalize( + resp.data, + schemas.task + ) + + dispatch(editTask(normTask)) + dispatch(notify(copy.taskUpdateSuccess())) + } catch (e) { + console.error(e) + const message = getErrorMessage(e) + dispatch(notify(copy.taskUpdateFailed(message))) + } +} + +export const updateTaskName = (name: string, taskID: string) => async ( + dispatch: Dispatch +) => { + try { + const resp = await api.patchTask({taskID, data: {name}}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const normTask = normalize( + resp.data, + schemas.task + ) + + dispatch(editTask(normTask)) + dispatch(notify(copy.taskUpdateSuccess())) + } catch (e) { + console.error(e) + const message = getErrorMessage(e) + dispatch(notify(copy.taskUpdateFailed(message))) + } +} + +export const deleteTask = (taskID: string) => async ( + dispatch: Dispatch +) => { + try { + const resp = await api.deleteTask({taskID}) + + if (resp.status !== 204) { + throw new Error(resp.data.message) + } + + dispatch(removeTask(taskID)) + dispatch(notify(copy.taskDeleteSuccess())) + } catch (e) { + console.error(e) + const message = getErrorMessage(e) + dispatch(notify(copy.taskDeleteFailed(message))) + } +} + +export const cloneTask = (task: Task) => async (dispatch: Dispatch) => { + try { + const resp = await api.getTask({taskID: task.id}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const newTask = await api.postTask({data: resp.data}) + + if (newTask.status !== 201) { + throw new Error(newTask.data.message) + } + + const normTask = normalize( + resp.data, + schemas.task + ) + + dispatch(notify(copy.taskCloneSuccess(task.name))) + dispatch(addTask(normTask)) + dispatch(checkTaskLimits()) + } catch (error) { + console.error(error) + if (isLimitError(error)) { + dispatch(notify(copy.resourceLimitReached('tasks'))) + } else { + const message = getErrorMessage(error) + dispatch(notify(copy.taskCloneFailed(task.name, message))) + } + } +} + +export const selectTaskByID = (id: string) => async ( + dispatch: Dispatch +): Promise => { + try { + const resp = await api.getTask({taskID: id}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const task = normalize(resp.data, schemas.task) + + dispatch(setCurrentTask(task)) + } catch (error) { + console.error(error) + dispatch(goToTasks()) + const message = getErrorMessage(error) + dispatch(notify(copy.taskNotFound(message))) + } +} + +export const setAllTaskOptionsByID = (taskID: string) => async ( + dispatch: Dispatch +): Promise => { + try { + const resp = await api.getTask({taskID}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const task = normalize(resp.data, schemas.task) + + dispatch(setAllTaskOptions(task)) + } catch (error) { + console.error(error) + dispatch(goToTasks()) + const message = getErrorMessage(error) + dispatch(notify(copy.taskNotFound(message))) + } +} + +export const selectTask = (taskID: string) => ( + dispatch: Dispatch, + getState: GetState +) => { + const org = getOrg(getState()) + + dispatch(push(`/orgs/${org.id}/tasks/${taskID}`)) +} + +export const goToTasks = () => ( + dispatch: Dispatch, + getState: GetState +) => { + const org = getOrg(getState()) + + dispatch(push(`/orgs/${org.id}/tasks`)) +} + +export const cancel = () => (dispatch: Dispatch) => { + dispatch(clearCurrentTask()) + dispatch(goBack()) +} + +export const updateScript = () => async ( + dispatch: Dispatch, + getState: GetState +) => { + try { + const state = getState() + const { + tasks: {currentScript: script, currentTask: task, taskOptions}, + } = state.resources + + const updatedTask: Partial & { + name: string + flux: string + token: string + } = { + flux: script, + name: taskOptions.name, + offset: taskOptions.offset, + token: null, + } + + if (taskOptions.taskScheduleType === TaskSchedule.interval) { + updatedTask.every = taskOptions.interval + updatedTask.cron = null + } else { + updatedTask.cron = taskOptions.cron + updatedTask.every = null + } + + const resp = await api.patchTask({taskID: task.id, data: updatedTask}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + dispatch(goToTasks()) + dispatch(clearCurrentTask()) + dispatch(notify(copy.taskUpdateSuccess())) + } catch (error) { + console.error(error) + const message = getErrorMessage(error) + dispatch(notify(copy.taskUpdateFailed(message))) + } +} + +export const saveNewScript = (script: string, preamble: string) => async ( + dispatch: Dispatch, + getState: GetState +): Promise => { + try { + const fluxScript = await insertPreambleInScript(script, preamble) + const org = getOrg(getState()) + const resp = await api.postTask({data: {orgID: org.id, flux: fluxScript}}) + if (resp.status !== 201) { + throw new Error(resp.data.message) + } + + dispatch(setNewScript('')) + dispatch(clearTask()) + dispatch(goToTasks()) + dispatch(notify(copy.taskCreatedSuccess())) + dispatch(checkTaskLimits()) + } catch (error) { + console.error(error) + if (isLimitError(error)) { + dispatch(notify(copy.resourceLimitReached('tasks'))) + } else { + const message = getErrorMessage(error) + dispatch(notify(copy.taskNotCreated(message))) + } + } +} + +export const getRuns = (taskID: string) => async ( + dispatch: Dispatch +): Promise => { + try { + dispatch(setRuns([], RemoteDataState.Loading)) + dispatch(selectTaskByID(taskID)) + const resp = await api.getTasksRuns({taskID}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const runsWithDuration = resp.data.runs.map(run => { + const finished = new Date(run.finishedAt) + const started = new Date(run.startedAt) + + return { + ...run, + duration: `${runDuration(finished, started)}`, + } + }) + + dispatch(setRuns(runsWithDuration, RemoteDataState.Done)) + } catch (error) { + console.error(error) + const message = getErrorMessage(error) + dispatch(notify(copy.taskGetFailed(message))) + dispatch(setRuns([], RemoteDataState.Error)) + } +} + +export const runTask = (taskID: string) => async ( + dispatch: Dispatch +) => { + try { + const resp = await api.postTasksRun({taskID}) + if (resp.status !== 201) { + throw new Error(resp.data.message) + } + + dispatch(notify(copy.taskRunSuccess())) + } catch (error) { + const message = getErrorMessage(error) + dispatch(notify(copy.taskRunFailed(message))) + console.error(error) + } +} + +export const getLogs = (taskID: string, runID: string) => async ( + dispatch: Dispatch +): Promise => { + try { + const resp = await api.getTasksRunsLogs({taskID, runID}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + dispatch(setLogs(resp.data.events)) + } catch (error) { + console.error(error) + dispatch(setLogs([])) + } +} + +export const convertToTemplate = (taskID: string) => async ( + dispatch +): Promise => { + try { + dispatch(setExportTemplate(RemoteDataState.Loading)) + const resp = await api.getTask({taskID}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const {entities, result} = normalize( + resp.data, + schemas.task + ) + + const taskTemplate = taskToTemplate(entities.tasks[result]) + + dispatch(setExportTemplate(RemoteDataState.Done, taskTemplate)) + } catch (error) { + dispatch(setExportTemplate(RemoteDataState.Error)) + dispatch(notify(copy.createTemplateFailed(error))) + } +} + +export const createTaskFromTemplate = (template: TaskTemplate) => async ( + dispatch: Dispatch, + getState: GetState +): Promise => { + try { + const org = getOrg(getState()) + + await createTaskFromTemplateAJAX(template, org.id) + + dispatch(getTasks()) + dispatch(notify(copy.importTaskSucceeded())) + dispatch(checkTaskLimits()) + } catch (error) { + if (isLimitError(error)) { + dispatch(notify(copy.resourceLimitReached('tasks'))) + } else { + dispatch(notify(copy.importTaskFailed(error))) + } + } +} + +export const runDuration = (finishedAt: Date, startedAt: Date): string => { + let timeTag = 'seconds' + + if (isNaN(finishedAt.getTime()) || isNaN(startedAt.getTime())) { + return '' + } + let diff = (finishedAt.getTime() - startedAt.getTime()) / 1000 + + if (diff > 60) { + diff = Math.round(diff / 60) + timeTag = 'minutes' + } + + return diff + ' ' + timeTag +} diff --git a/ui/src/tasks/components/TaskCard.test.tsx b/ui/src/tasks/components/TaskCard.test.tsx index 799ff62c92..9594858c10 100644 --- a/ui/src/tasks/components/TaskCard.test.tsx +++ b/ui/src/tasks/components/TaskCard.test.tsx @@ -22,7 +22,7 @@ const setup = (override = {}) => { onFilterChange: jest.fn(), onUpdate: jest.fn(), onAddTaskLabel: jest.fn(), - onRemoveTaskLabel: jest.fn(), + onDeleteTaskLabel: jest.fn(), onCreateLabel: jest.fn(), labels: [], // all labels ...override, diff --git a/ui/src/tasks/components/TaskCard.tsx b/ui/src/tasks/components/TaskCard.tsx index 30e478a2c1..d4fe121c3e 100644 --- a/ui/src/tasks/components/TaskCard.tsx +++ b/ui/src/tasks/components/TaskCard.tsx @@ -17,8 +17,12 @@ import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels' import LastRunTaskStatus from 'src/shared/components/lastRunTaskStatus/LastRunTaskStatus' // Actions -import {addTaskLabelAsync, removeTaskLabelAsync} from 'src/tasks/actions' -import {createLabel as createLabelAsync} from 'src/labels/actions' +import { + addTaskLabel, + deleteTaskLabel, + selectTask, +} from 'src/tasks/actions/thunks' +import {createLabel} from 'src/labels/actions' // Selectors import {viewableLabels} from 'src/labels/selectors' @@ -34,7 +38,7 @@ interface PassedProps { task: Task onActivate: (task: Task) => void onDelete: (task: Task) => void - onSelect: (task: Task) => void + onSelect: typeof selectTask onClone: (task: Task) => void onRunTask: (taskID: string) => void onUpdate: (name: string, taskID: string) => void @@ -46,9 +50,9 @@ interface StateProps { } interface DispatchProps { - onAddTaskLabel: typeof addTaskLabelAsync - onRemoveTaskLabel: typeof removeTaskLabelAsync - onCreateLabel: typeof createLabelAsync + onAddTaskLabel: typeof addTaskLabel + onDeleteTaskLabel: typeof deleteTaskLabel + onCreateLabel: typeof createLabel } type Props = PassedProps & StateProps & DispatchProps @@ -139,7 +143,7 @@ export class TaskCard extends PureComponent { private handleNameClick = (e: MouseEvent) => { e.preventDefault() - this.props.onSelect(this.props.task) + this.props.onSelect(this.props.task.id) } private handleViewRuns = () => { @@ -190,9 +194,9 @@ export class TaskCard extends PureComponent { } private handleRemoveLabel = (label: Label) => { - const {task, onRemoveTaskLabel} = this.props + const {task, onDeleteTaskLabel} = this.props - onRemoveTaskLabel(task.id, label) + onDeleteTaskLabel(task.id, label) } private handleCreateLabel = (label: Label) => { @@ -239,9 +243,9 @@ const mstp = ({labels}: AppState): StateProps => { } const mdtp: DispatchProps = { - onCreateLabel: createLabelAsync, - onAddTaskLabel: addTaskLabelAsync, - onRemoveTaskLabel: removeTaskLabelAsync, + onCreateLabel: createLabel, + onAddTaskLabel: addTaskLabel, + onDeleteTaskLabel: deleteTaskLabel, } export default connect( diff --git a/ui/src/tasks/components/TaskExportOverlay.tsx b/ui/src/tasks/components/TaskExportOverlay.tsx index b37528cf49..91ecfb75f8 100644 --- a/ui/src/tasks/components/TaskExportOverlay.tsx +++ b/ui/src/tasks/components/TaskExportOverlay.tsx @@ -6,7 +6,7 @@ import {connect} from 'react-redux' import ExportOverlay from 'src/shared/components/ExportOverlay' // Actions -import {convertToTemplate as convertToTemplateAction} from 'src/tasks/actions' +import {convertToTemplate as convertToTemplateAction} from 'src/tasks/actions/thunks' import {clearExportTemplate as clearExportTemplateAction} from 'src/templates/actions' // Types diff --git a/ui/src/tasks/components/TaskForm.tsx b/ui/src/tasks/components/TaskForm.tsx index 3229b8acb1..6118e3fafc 100644 --- a/ui/src/tasks/components/TaskForm.tsx +++ b/ui/src/tasks/components/TaskForm.tsx @@ -25,8 +25,7 @@ import { AlignItems, ComponentSize, } from '@influxdata/clockface' -import {ResourceType} from 'src/types' -import {TaskOptions, TaskSchedule} from 'src/utils/taskOptionsToFluxScript' +import {ResourceType, TaskOptions, TaskSchedule} from 'src/types' interface Props { taskOptions: TaskOptions diff --git a/ui/src/tasks/components/TaskHeader.tsx b/ui/src/tasks/components/TaskHeader.tsx index 96898c9a40..8590e3d772 100644 --- a/ui/src/tasks/components/TaskHeader.tsx +++ b/ui/src/tasks/components/TaskHeader.tsx @@ -21,31 +21,34 @@ interface Props { export default class TaskHeader extends PureComponent { public render() { + const {onCancel, onSave, title} = this.props return ( - +