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
pull/16451/head
Andrew Watkins 2020-01-08 13:03:36 -08:00 committed by GitHub
parent 625ab8999d
commit 62398d04b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1084 additions and 966 deletions

View File

@ -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))

View File

@ -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<Props & WithRouterProps> {
}
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)

View File

@ -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>(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 = <R extends {labels?: Label[]}>(resource: R): R => {
return {
...resource,
labels: (resource.labels || []).map(addLabelDefaults),
}
}

View File

@ -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'

View File

@ -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

View File

@ -71,12 +71,12 @@ export const rootReducer = combineReducers<ReducerState>({
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,

View File

@ -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<typeof setTasks>
| ReturnType<typeof editTask>
| ReturnType<typeof setTaskOption>
| ReturnType<typeof setAllTaskOptions>
| ReturnType<typeof setSearchTerm>
| ReturnType<typeof setCurrentScript>
| ReturnType<typeof setCurrentTask>
| ReturnType<typeof setShowInactive>
| ReturnType<typeof clearTask>
| ReturnType<typeof setRuns>
| ReturnType<typeof setLogs>
| ReturnType<typeof editTask>
| ReturnType<typeof setNewScript>
| ReturnType<typeof clearCurrentTask>
| ReturnType<typeof removeTask>
| ReturnType<typeof addTask>
// R is the type of the value of the "result" key in normalizr's normalization
type TasksSchema<R extends string | string[]> = NormalizedSchema<
TaskEntities,
R
>
export const setTasks = (
status: RemoteDataState,
schema?: TasksSchema<string[]>
) =>
({
type: SET_TASKS,
status,
schema,
} as const)
export const addTask = (schema: TasksSchema<string>) =>
({
type: ADD_TASK,
schema,
} as const)
export const editTask = (schema: TasksSchema<string>) =>
({
type: EDIT_TASK,
schema,
} as const)
export const removeTask = (id: string) =>
({
type: REMOVE_TASK,
id,
} as const)
export const setCurrentTask = (schema: TasksSchema<string>) =>
({
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<string>) =>
({
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)

View File

@ -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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<Task> & {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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
}

View File

@ -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<typeof getTasks>
type ExternalActions = NotifyAction | ReturnType<typeof checkTaskLimits>
// Thunks
export const getTasks = () => async (
dispatch: Dispatch<TaskAction | NotifyAction>,
getState: GetState
): Promise<void> => {
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<Task, TaskEntities, string[]>(
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<Action>
): Promise<void> => {
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<Task, TaskEntities, string>(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<Action>
): Promise<void> => {
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<Task, TaskEntities, string>(resp.data, schemas.task)
dispatch(editTask(task))
} catch (error) {
console.error(error)
dispatch(notify(copy.removeTaskLabelFailed()))
}
}
export const updateTaskStatus = (task: Task) => async (
dispatch: Dispatch<Action>
) => {
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<Task, TaskEntities, string>(
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<Action>
) => {
try {
const resp = await api.patchTask({taskID, data: {name}})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
const normTask = normalize<Task, TaskEntities, string>(
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<Action>
) => {
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<Action>) => {
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<Task, TaskEntities, string>(
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<Action>
): Promise<void> => {
try {
const resp = await api.getTask({taskID: id})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
const task = normalize<Task, TaskEntities, string>(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<Action>
): Promise<void> => {
try {
const resp = await api.getTask({taskID})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
const task = normalize<Task, TaskEntities, string>(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<Action>,
getState: GetState
) => {
const org = getOrg(getState())
dispatch(push(`/orgs/${org.id}/tasks/${taskID}`))
}
export const goToTasks = () => (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const org = getOrg(getState())
dispatch(push(`/orgs/${org.id}/tasks`))
}
export const cancel = () => (dispatch: Dispatch<Action>) => {
dispatch(clearCurrentTask())
dispatch(goBack())
}
export const updateScript = () => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
try {
const state = getState()
const {
tasks: {currentScript: script, currentTask: task, taskOptions},
} = state.resources
const updatedTask: Partial<Task> & {
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<Action>,
getState: GetState
): Promise<void> => {
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<Action>
): Promise<void> => {
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<Action>
) => {
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<Action>
): Promise<void> => {
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<void> => {
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<Task, TaskEntities, string>(
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<Action>,
getState: GetState
): Promise<void> => {
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
}

View File

@ -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,

View File

@ -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<Props & WithRouterProps> {
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<Props & WithRouterProps> {
}
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<StateProps, DispatchProps, PassedProps>(

View File

@ -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

View File

@ -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

View File

@ -21,31 +21,34 @@ interface Props {
export default class TaskHeader extends PureComponent<Props> {
public render() {
const {onCancel, onSave, title} = this.props
return (
<Page.Header fullWidth={true}>
<Page.HeaderLeft>
<PageTitleWithOrg title={this.props.title} />
<PageTitleWithOrg title={title} />
</Page.HeaderLeft>
<Page.HeaderRight>
<Button
color={ComponentColor.Default}
text="Cancel"
onClick={this.props.onCancel}
onClick={onCancel}
testID="task-cancel-btn"
/>
<Button
color={ComponentColor.Success}
text="Save"
status={
this.props.canSubmit
? ComponentStatus.Default
: ComponentStatus.Disabled
}
onClick={this.props.onSave}
status={this.status}
onClick={onSave}
testID="task-save-btn"
/>
</Page.HeaderRight>
</Page.Header>
)
}
private get status() {
return this.props.canSubmit
? ComponentStatus.Default
: ComponentStatus.Disabled
}
}

View File

@ -16,7 +16,7 @@ import TemplateBrowserEmpty from 'src/tasks/components/TemplateBrowserEmpty'
import GetResources from 'src/shared/components/GetResources'
// Actions
import {createTaskFromTemplate as createTaskFromTemplateAction} from 'src/tasks/actions'
import {createTaskFromTemplate as createTaskFromTemplateAction} from 'src/tasks/actions/thunks'
import {getTemplateByID} from 'src/templates/actions'
// Types

View File

@ -9,7 +9,7 @@ import ImportOverlay from 'src/shared/components/ImportOverlay'
import {invalidJSON} from 'src/shared/copy/notifications'
// Actions
import {createTaskFromTemplate as createTaskFromTemplateAction} from 'src/tasks/actions/'
import {createTaskFromTemplate as createTaskFromTemplateAction} from 'src/tasks/actions/thunks'
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Types

View File

@ -1,6 +1,5 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
@ -19,7 +18,7 @@ import {
} from '@influxdata/clockface'
// Actions
import {getRuns, runTask} from 'src/tasks/actions'
import {getRuns, runTask} from 'src/tasks/actions/thunks'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
@ -145,12 +144,12 @@ class TaskRunsPage extends PureComponent<Props & WithRouterProps, State> {
}
const mstp = (state: AppState): StateProps => {
const {tasks} = state
const {runs, runStatus, currentTask} = state.resources.tasks
return {
runs: tasks.runs,
runStatus: tasks.runStatus,
currentTask: tasks.currentTask,
runs,
runStatus,
currentTask,
}
}

View File

@ -8,7 +8,7 @@ import {Overlay, IndexList} from '@influxdata/clockface'
import RunLogsOverlay from 'src/tasks/components/RunLogsList'
// Actions
import {getLogs} from 'src/tasks/actions'
import {getLogs} from 'src/tasks/actions/thunks'
// Types
import {ComponentSize, ComponentColor, Button} from '@influxdata/clockface'
@ -102,9 +102,8 @@ class TaskRunsRow extends PureComponent<Props, State> {
}
const mstp = (state: AppState): StateProps => {
const {
tasks: {logs},
} = state
const {logs} = state.resources.tasks
return {logs}
}

View File

@ -6,7 +6,7 @@ import {Form, Input, Grid} from '@influxdata/clockface'
// Types
import {Columns, InputType} from '@influxdata/clockface'
import {TaskSchedule} from 'src/utils/taskOptionsToFluxScript'
import {TaskSchedule} from 'src/types'
interface Props {
schedule: TaskSchedule

View File

@ -1,6 +1,5 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
import memoizeOne from 'memoize-one'
// Components
@ -13,11 +12,7 @@ import {Task} from 'src/types'
import {SortTypes} from 'src/shared/utils/sort'
import {Sort} from '@influxdata/clockface'
import {
addTaskLabelAsync,
removeTaskLabelAsync,
runTask,
} from 'src/tasks/actions'
import {selectTask, addTaskLabel, runTask} from 'src/tasks/actions/thunks'
import {checkTaskLimits as checkTaskLimitsAction} from 'src/cloud/actions/limits'
// Selectors
@ -29,12 +24,11 @@ interface Props {
onActivate: (task: Task) => void
onDelete: (task: Task) => void
onCreate: () => void
onSelect: (task: Task) => void
onClone: (task: Task) => void
onFilterChange: (searchTerm: string) => void
totalCount: number
onRemoveTaskLabel: typeof removeTaskLabelAsync
onAddTaskLabel: typeof addTaskLabelAsync
onSelect: typeof selectTask
onAddTaskLabel: typeof addTaskLabel
onRunTask: typeof runTask
onUpdate: (name: string, taskID: string) => void
filterComponent?: JSX.Element

View File

@ -28,25 +28,28 @@ const FluxMonacoEditor = Loadable({
// Actions
import {
updateScript,
selectTaskByID,
setCurrentScript,
cancel,
setTaskOption,
clearTask,
} from 'src/tasks/actions/creators'
import {
updateScript,
selectTaskByID,
cancel,
setAllTaskOptionsByID,
} from 'src/tasks/actions'
} from 'src/tasks/actions/thunks'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
// Types
import {
AppState,
Task,
TaskOptions,
TaskOptionKeys,
TaskSchedule,
} from 'src/utils/taskOptionsToFluxScript'
import {AppState, Task} from 'src/types'
} from 'src/types'
interface OwnProps {
router: InjectedRouter
@ -165,11 +168,13 @@ class TaskEditPage extends PureComponent<Props> {
}
}
const mstp = ({tasks}: AppState): StateProps => {
const mstp = (state: AppState): StateProps => {
const {taskOptions, currentScript, currentTask} = state.resources.tasks
return {
taskOptions: tasks.taskOptions,
currentScript: tasks.currentScript,
currentTask: tasks.currentTask,
taskOptions,
currentScript,
currentTask,
}
}

View File

@ -29,11 +29,10 @@ const FluxMonacoEditor = Loadable({
// Actions
import {
setNewScript,
saveNewScript,
setTaskOption,
clearTask,
cancel,
} from 'src/tasks/actions'
} from 'src/tasks/actions/creators'
import {saveNewScript, cancel} from 'src/tasks/actions/thunks'
// Utils
import {
@ -43,12 +42,7 @@ import {
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
// Types
import {AppState} from 'src/types'
import {
TaskOptions,
TaskOptionKeys,
TaskSchedule,
} from 'src/utils/taskOptionsToFluxScript'
import {AppState, TaskOptions, TaskOptionKeys, TaskSchedule} from 'src/types'
interface OwnProps {
router: InjectedRouter
@ -73,6 +67,7 @@ class TaskPage extends PureComponent<Props> {
constructor(props) {
super(props)
}
public componentDidMount() {
this.props.setTaskOption({
key: 'taskScheduleType',
@ -141,8 +136,8 @@ class TaskPage extends PureComponent<Props> {
this.props.setNewScript(script)
}
private handleChangeScheduleType = (schedule: TaskSchedule) => {
this.props.setTaskOption({key: 'taskScheduleType', value: schedule})
private handleChangeScheduleType = (value: TaskSchedule) => {
this.props.setTaskOption({key: 'taskScheduleType', value})
}
private handleSave = () => {
@ -167,10 +162,13 @@ class TaskPage extends PureComponent<Props> {
}
}
const mstp = ({tasks}: AppState): StateProps => {
const mstp = (state: AppState): StateProps => {
const {tasks} = state.resources
const {taskOptions, newScript} = tasks
return {
taskOptions: tasks.taskOptions,
newScript: tasks.newScript,
taskOptions,
newScript,
}
}

View File

@ -1,7 +1,6 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
// Components
import TasksHeader from 'src/tasks/components/TasksHeader'
@ -24,12 +23,15 @@ import {
deleteTask,
selectTask,
cloneTask,
addTaskLabel,
runTask,
} from 'src/tasks/actions/thunks'
import {
setSearchTerm as setSearchTermAction,
setShowInactive as setShowInactiveAction,
addTaskLabelAsync,
removeTaskLabelAsync,
runTask,
} from 'src/tasks/actions'
} from 'src/tasks/actions/creators'
import {
checkTaskLimits as checkTasksLimitsAction,
LimitStatus,
@ -42,6 +44,9 @@ import {Sort} from '@influxdata/clockface'
import {SortTypes} from 'src/shared/utils/sort'
import {extractTaskLimits} from 'src/cloud/utils/limits'
// Selectors
import {getAll} from 'src/resources/selectors'
interface PassedInProps {
router: InjectedRouter
}
@ -54,8 +59,7 @@ interface ConnectedDispatchProps {
selectTask: typeof selectTask
setSearchTerm: typeof setSearchTermAction
setShowInactive: typeof setShowInactiveAction
onAddTaskLabel: typeof addTaskLabelAsync
onRemoveTaskLabel: typeof removeTaskLabelAsync
onAddTaskLabel: typeof addTaskLabel
onRunTask: typeof runTask
checkTaskLimits: typeof checkTasksLimitsAction
}
@ -105,13 +109,13 @@ class TasksPage extends PureComponent<Props, State> {
public render(): JSX.Element {
const {sortKey, sortDirection, sortType} = this.state
const {
selectTask,
setSearchTerm,
updateTaskName,
searchTerm,
setShowInactive,
showInactive,
onAddTaskLabel,
onRemoveTaskLabel,
onRunTask,
checkTaskLimits,
limitStatus,
@ -150,9 +154,8 @@ class TasksPage extends PureComponent<Props, State> {
onDelete={this.handleDelete}
onCreate={this.handleCreateTask}
onClone={this.handleClone}
onSelect={this.props.selectTask}
onSelect={selectTask}
onAddTaskLabel={onAddTaskLabel}
onRemoveTaskLabel={onRemoveTaskLabel}
onRunTask={onRunTask}
onFilterChange={setSearchTerm}
filterComponent={this.search}
@ -194,12 +197,11 @@ class TasksPage extends PureComponent<Props, State> {
}
private handleDelete = (task: Task) => {
this.props.deleteTask(task)
this.props.deleteTask(task.id)
}
private handleClone = (task: Task) => {
const {tasks} = this.props
this.props.cloneTask(task, tasks)
this.props.cloneTask(task)
}
private handleCreateTask = () => {
@ -281,12 +283,15 @@ class TasksPage extends PureComponent<Props, State> {
}
}
const mstp = ({
tasks: {status, list, searchTerm, showInactive},
cloud: {limits},
}: AppState): ConnectedStateProps => {
const mstp = (state: AppState): ConnectedStateProps => {
const {
resources,
cloud: {limits},
} = state
const {status, searchTerm, showInactive} = resources.tasks
return {
tasks: list,
tasks: getAll<Task>(state, ResourceType.Tasks),
status: status,
searchTerm,
showInactive,
@ -302,8 +307,7 @@ const mdtp: ConnectedDispatchProps = {
cloneTask,
setSearchTerm: setSearchTermAction,
setShowInactive: setShowInactiveAction,
onRemoveTaskLabel: removeTaskLabelAsync,
onAddTaskLabel: addTaskLabelAsync,
onAddTaskLabel: addTaskLabel,
onRunTask: runTask,
checkTaskLimits: checkTasksLimitsAction,
}

View File

@ -0,0 +1,32 @@
import {
RemoteDataState,
ResourceState,
TaskOptions,
TaskSchedule,
} from 'src/types'
export const initialState = (): ResourceState['tasks'] => ({
allIDs: [],
byID: {},
status: RemoteDataState.NotStarted,
newScript: '',
currentTask: null,
currentScript: '',
searchTerm: '',
showInactive: true,
taskOptions: defaultOptions,
runStatus: RemoteDataState.NotStarted,
runs: [],
logs: [],
})
export const defaultOptions: TaskOptions = {
name: '',
interval: '',
offset: '',
cron: '',
taskScheduleType: TaskSchedule.unselected,
orgID: '',
toBucketName: '',
toOrgName: '',
}

View File

@ -1,82 +1,94 @@
import {TaskOptions, TaskSchedule} from 'src/utils/taskOptionsToFluxScript'
// Libraries
import {produce} from 'immer'
// Types
import {Action} from 'src/tasks/actions'
import {Task, LogEvent, Run} from 'src/types'
import {RemoteDataState} from '@influxdata/clockface'
import {
Action,
ADD_TASK,
SET_TASKS,
CLEAR_TASK,
CLEAR_CURRENT_TASK,
SET_RUNS,
SET_TASK_OPTION,
SET_ALL_TASK_OPTIONS,
SET_NEW_SCRIPT,
SET_CURRENT_SCRIPT,
SET_CURRENT_TASK,
SET_SEARCH_TERM,
SET_SHOW_INACTIVE,
SET_LOGS,
EDIT_TASK,
REMOVE_TASK,
} from 'src/tasks/actions/creators'
import {ResourceType, ResourceState, TaskSchedule, Task} from 'src/types'
export interface TasksState {
status: RemoteDataState
list: Task[]
newScript: string
currentScript: string
currentTask: Task
searchTerm: string
showInactive: boolean
taskOptions: TaskOptions
runs: Run[]
runStatus: RemoteDataState
logs: LogEvent[]
}
// Utils
import {initialState, defaultOptions} from 'src/tasks/reducers/helpers'
import {
setResource,
editResource,
removeResource,
addResource,
} from 'src/resources/reducers/helpers'
export const defaultTaskOptions: TaskOptions = {
name: '',
interval: '',
offset: '',
cron: '',
taskScheduleType: TaskSchedule.unselected,
orgID: '',
toBucketName: '',
toOrgName: '',
}
export const defaultState: TasksState = {
status: RemoteDataState.NotStarted,
list: [],
newScript: '',
currentTask: null,
currentScript: '',
searchTerm: '',
showInactive: true,
taskOptions: defaultTaskOptions,
runs: [],
runStatus: RemoteDataState.NotStarted,
logs: [],
}
type TasksState = ResourceState['tasks']
export default (
state: TasksState = defaultState,
state: TasksState = initialState(),
action: Action
): TasksState => {
switch (action.type) {
case 'SET_TASKS':
return {
...state,
list: action.payload.tasks,
status: RemoteDataState.Done,
}
case 'SET_TASKS_STATUS':
return {
...state,
status: action.payload.status,
}
case 'CLEAR_TASK':
return {
...state,
taskOptions: defaultTaskOptions,
currentScript: '',
newScript: '',
}
case 'SET_ALL_TASK_OPTIONS':
const {name, every, cron, orgID, offset} = action.payload
let taskScheduleType = TaskSchedule.interval
if (cron) {
taskScheduleType = TaskSchedule.cron
): TasksState =>
produce(state, draftState => {
switch (action.type) {
case SET_TASKS: {
setResource<Task>(draftState, action, ResourceType.Tasks)
return
}
return {
...state,
taskOptions: {
case EDIT_TASK: {
editResource<Task>(draftState, action, ResourceType.Tasks)
return
}
case REMOVE_TASK: {
removeResource<Task>(draftState, action)
return
}
case ADD_TASK: {
addResource<Task>(draftState, action, ResourceType.Tasks)
return
}
case CLEAR_TASK: {
draftState.taskOptions = defaultOptions
draftState.currentScript = ''
draftState.newScript = ''
return
}
case CLEAR_CURRENT_TASK: {
draftState.currentScript = ''
draftState.currentTask = null
return
}
case SET_ALL_TASK_OPTIONS: {
const {schema} = action
const {entities, result} = schema
const {name, every, cron, orgID, offset} = entities.tasks[result]
let taskScheduleType = TaskSchedule.interval
if (cron) {
taskScheduleType = TaskSchedule.cron
}
draftState.taskOptions = {
...state.taskOptions,
name,
cron,
@ -84,43 +96,72 @@ export default (
orgID,
taskScheduleType,
offset,
},
}
case 'SET_TASK_OPTION':
const {key, value} = action.payload
return {
...state,
taskOptions: {...state.taskOptions, [key]: value},
}
case 'SET_NEW_SCRIPT':
return {...state, newScript: action.payload.script}
case 'SET_CURRENT_SCRIPT':
return {...state, currentScript: action.payload.script}
case 'SET_CURRENT_TASK':
const {task} = action.payload
let currentScript = ''
if (task) {
currentScript = task.flux
}
return {...state, currentScript, currentTask: task}
case 'SET_SEARCH_TERM':
const {searchTerm} = action.payload
return {...state, searchTerm}
case 'SET_SHOW_INACTIVE':
return {...state, showInactive: !state.showInactive}
case 'UPDATE_TASK': {
const {task} = action.payload
const tasks = state.list.map(t => (t.id === task.id ? task : t))
}
return {...state, list: tasks}
return
}
case SET_TASK_OPTION: {
const {key, value} = action
draftState.taskOptions[`${key}`] = value
return
}
case SET_NEW_SCRIPT: {
draftState.newScript = action.script
return
}
case SET_CURRENT_SCRIPT: {
draftState.currentScript = action.script
return
}
case SET_CURRENT_TASK: {
const {schema} = action
const {entities, result} = schema
const task = entities.tasks[result]
const currentScript = task.flux || ''
draftState.currentScript = currentScript
draftState.currentTask = task
return
}
case SET_SEARCH_TERM: {
const {searchTerm} = action
draftState.searchTerm = searchTerm
return
}
case SET_SHOW_INACTIVE: {
draftState.showInactive = !state.showInactive
return
}
case SET_RUNS: {
const {runs, runStatus} = action
draftState.runs = runs
draftState.runStatus = runStatus
return
}
case SET_LOGS: {
draftState.logs = action.logs
return
}
}
case 'SET_RUNS':
const {runs, runStatus} = action.payload
return {...state, runs, runStatus}
case 'SET_LOGS':
const {logs} = action.payload
return {...state, logs}
default:
return state
}
}
})

View File

@ -1,26 +1,28 @@
import tasksReducer, {
defaultState,
defaultTaskOptions,
} from 'src/tasks/reducers'
import {setTaskOption} from 'src/tasks/actions'
import {TaskSchedule} from 'src/utils/taskOptionsToFluxScript'
import tasksReducer from 'src/tasks/reducers'
import {setTaskOption} from 'src/tasks/actions/creators'
// Helpers
import {initialState, defaultOptions} from 'src/tasks/reducers/helpers'
// Types
import {TaskSchedule} from 'src/types'
describe('tasksReducer', () => {
describe('setTaskOption', () => {
it('should not clear the cron property from the task options when interval is selected', () => {
const initialState = defaultState
const state = initialState()
const cron = '0 2 * * *'
initialState.taskOptions = {...defaultTaskOptions, cron}
state.taskOptions = {...defaultOptions, cron}
const actual = tasksReducer(
initialState,
state,
setTaskOption({key: 'taskScheduleType', value: TaskSchedule.interval})
)
const expected = {
...defaultState,
...state,
taskOptions: {
...defaultTaskOptions,
...defaultOptions,
taskScheduleType: TaskSchedule.interval,
cron,
},
@ -30,19 +32,19 @@ describe('tasksReducer', () => {
})
it('should not clear the interval property from the task options when cron is selected', () => {
const initialState = defaultState
const state = initialState()
const interval = '24h'
initialState.taskOptions = {...defaultTaskOptions, interval} // todo(docmerlin): allow for time units larger than 1d, right now h is the longest unit our s
state.taskOptions = {...defaultOptions, interval} // todo(docmerlin): allow for time units larger than 1d, right now h is the longest unit our s
const actual = tasksReducer(
initialState,
state,
setTaskOption({key: 'taskScheduleType', value: TaskSchedule.cron})
)
const expected = {
...defaultState,
...state,
taskOptions: {
...defaultTaskOptions,
...defaultOptions,
taskScheduleType: TaskSchedule.cron,
interval,
},

View File

@ -30,7 +30,7 @@ import * as copy from 'src/shared/copy/notifications'
import {client} from 'src/utils/api'
import {createDashboardFromTemplate} from 'src/dashboards/actions'
import {createVariableFromTemplate} from 'src/variables/actions'
import {createTaskFromTemplate} from 'src/tasks/actions'
import {createTaskFromTemplate} from 'src/tasks/actions/thunks'
// Selectors
import {getOrg} from 'src/organizations/selectors'

View File

@ -1,18 +1,9 @@
import _, {get} from 'lodash'
import {
DashboardTemplate,
TemplateType,
CellIncluded,
LabelIncluded,
ViewIncluded,
TaskTemplate,
TemplateBase,
Task,
VariableTemplate,
Variable,
} from 'src/types'
import {IDashboard, Cell} from '@influxdata/influx'
import {client} from 'src/utils/api'
// Libraries
import {get, isEmpty, flatMap} from 'lodash'
import {normalize} from 'normalizr'
// Schemas
import * as schemas from 'src/schemas'
// Utils
import {
@ -24,9 +15,9 @@ import {
hasLabelsRelationships,
getLabelRelationships,
} from 'src/templates/utils/'
import {addDefaults} from 'src/tasks/actions'
import {addVariableDefaults} from 'src/variables/actions'
import {addLabelDefaults} from 'src/labels/utils'
// API
import {
getTask as apiGetTask,
@ -39,8 +30,25 @@ import {
postVariable as apiPostVariable,
postVariablesLabel as apiPostVariablesLabel,
} from 'src/client'
// Create Dashboard Templates
import {client} from 'src/utils/api'
// Types
import {
DashboardTemplate,
TemplateType,
CellIncluded,
LabelIncluded,
ViewIncluded,
TaskTemplate,
TemplateBase,
TaskEntities,
Task,
VariableTemplate,
Variable,
} from 'src/types'
import {IDashboard, Cell} from '@influxdata/influx'
// Create Dashboard Templates
export const createDashboardFromTemplate = async (
template: DashboardTemplate,
orgID: string
@ -103,11 +111,11 @@ const createLabelsFromTemplate = async <T extends TemplateBase>(
hasLabelsRelationships(r)
)
if (_.isEmpty(labeledResources)) {
if (isEmpty(labeledResources)) {
return {}
}
const labelRelationships = _.flatMap(labeledResources, r =>
const labelRelationships = flatMap(labeledResources, r =>
getLabelRelationships(r)
)
@ -129,8 +137,8 @@ const createLabelsFromTemplate = async <T extends TemplateBase>(
includedLabels
).map(l => ({
orgID,
name: _.get(l, 'attributes.name', ''),
properties: _.get(l, 'attributes.properties', {}),
name: get(l, 'attributes.name', ''),
properties: get(l, 'attributes.properties', {}),
}))
const promisedLabels = foundLabelsToCreate.map(async lab => {
@ -299,7 +307,12 @@ export const createTaskFromTemplate = async (
throw new Error(postResp.data.message)
}
const postedTask = addDefaults(postResp.data)
const {entities, result} = normalize<Task, TaskEntities, string>(
postResp.data,
schemas.task
)
const postedTask = entities.tasks[result]
// associate imported label.id with created label
const labelMap = await createLabelsFromTemplate(template, orgID)
@ -312,9 +325,7 @@ export const createTaskFromTemplate = async (
throw new Error(resp.data.message)
}
const task = addDefaults(resp.data)
return task
return postedTask
} catch (e) {
console.error(e)
}

View File

@ -6,6 +6,7 @@ import {
RemoteDataState,
Telegraf,
Scraper,
TasksState,
} from 'src/types'
export enum ResourceType {
@ -42,21 +43,13 @@ export interface TelegrafsState extends NormalizedState<Telegraf> {
currentConfig: {status: RemoteDataState; item: string}
}
const {
Authorizations,
Buckets,
Members,
Orgs,
Telegrafs,
Scrapers,
} = ResourceType
// ResourceState defines the types for normalized resources
export interface ResourceState {
[Authorizations]: NormalizedState<Authorization>
[Buckets]: NormalizedState<Bucket>
[Members]: NormalizedState<Member>
[Orgs]: OrgsState
[Telegrafs]: TelegrafsState
[Scrapers]: NormalizedState<Scraper>
[ResourceType.Authorizations]: NormalizedState<Authorization>
[ResourceType.Buckets]: NormalizedState<Bucket>
[ResourceType.Members]: NormalizedState<Member>
[ResourceType.Orgs]: OrgsState
[ResourceType.Telegrafs]: TelegrafsState
[ResourceType.Scrapers]: NormalizedState<Scraper>
[ResourceType.Tasks]: TasksState
}

View File

@ -1,5 +1,6 @@
// Types
import {
Task,
Telegraf,
Member,
Bucket,
@ -11,7 +12,7 @@ import {
// TODO: make these Entities generic
// AuthEntities defines the result of normalizr's normalization
// of the "authorization" resource
// of the "authorizations" resource
export interface AuthEntities {
buckets: {
[uuid: string]: Authorization
@ -19,7 +20,7 @@ export interface AuthEntities {
}
// BucketEntities defines the result of normalizr's normalization
// of the "bucket" resource
// of the "buckets" resource
export interface BucketEntities {
buckets: {
[uuid: string]: Bucket
@ -27,7 +28,7 @@ export interface BucketEntities {
}
// MemberEntities defines the result of normalizr's normalization
// of the "member" resource
// of the "members" resource
export interface MemberEntities {
members: {
[uuid: string]: Member
@ -35,7 +36,7 @@ export interface MemberEntities {
}
// OrgEntities defines the result of normalizr's normalization
// of the "organization" resource
// of the "organizations" resource
export interface OrgEntities {
orgs: {
[uuid: string]: Organization
@ -43,7 +44,7 @@ export interface OrgEntities {
}
// TelegrafEntities defines the result of normalizr's normalization
// of the "telegraf" resource
// of the "telegrafs" resource
export interface TelegrafEntities {
telegrafs: {
[uuid: string]: Telegraf
@ -51,9 +52,17 @@ export interface TelegrafEntities {
}
// ScraperEntities defines the result of normalizr's normalization
// of the "scraper" resource
// of the "scrapers" resource
export interface ScraperEntities {
scrapers: {
[uuid: string]: Scraper
}
}
// TaskEntities defines the result of normalizr's normalization
// of the "tasks" resource
export interface TaskEntities {
tasks: {
[uuid: string]: Task
}
}

View File

@ -3,7 +3,6 @@ import {Notification} from 'src/types'
import {TimeRange} from 'src/types/queries'
import {TimeMachinesState} from 'src/timeMachine/reducers'
import {AppState as AppPresentationState} from 'src/shared/reducers/app'
import {TasksState} from 'src/tasks/reducers'
import {RouterState} from 'react-router-redux'
import {MeState} from 'src/shared/reducers/me'
import {NoteEditorState} from 'src/dashboards/reducers/notes'
@ -54,7 +53,6 @@ export interface AppState {
resources: ResourceState
routing: RouterState
rules: NotificationRulesState
tasks: TasksState
telegrafEditorPlugins: TelegrafEditorPluginState
telegrafEditorActivePlugins: TelegrafEditorActivePluginState
plugins: PluginResourceState

View File

@ -1,6 +1,36 @@
import {Task as ITask} from 'src/client'
import {Label} from 'src/types'
import {Label, NormalizedState, Run, RemoteDataState, LogEvent} from 'src/types'
export interface Task extends ITask {
labels?: Label[]
}
export interface TaskOptions {
name: string
interval: string
cron: string
offset: string
taskScheduleType: TaskSchedule
orgID: string
toOrgName: string
toBucketName: string
}
export interface TasksState extends NormalizedState<Task> {
newScript: string
currentScript: string
currentTask: Task
searchTerm: string
showInactive: boolean
taskOptions: TaskOptions
runs: Run[]
runStatus: RemoteDataState
logs: LogEvent[]
}
export enum TaskSchedule {
interval = 'interval',
cron = 'cron',
unselected = '',
}
export type TaskOptionKeys = keyof TaskOptions

View File

@ -6,25 +6,25 @@ import {getAPIBasepath} from 'src/utils/basepath'
const basePath = `${getAPIBasepath()}/api/v2`
export const getErrorMessage = (e: any) => {
let message = get(e, 'response.data.error.message', '')
let message = get(e, 'response.data.error.message')
if (message === '') {
message = get(e, 'response.data.error', '')
if (!message) {
message = get(e, 'response.data.error')
}
if (message === '') {
message = get(e, 'response.headers.x-influx-error', '')
if (!message) {
message = get(e, 'response.headers.x-influx-error')
}
if (message === '') {
message = get(e, 'response.data.message', '')
if (!message) {
message = get(e, 'response.data.message')
}
if (message === '') {
message = get(e, 'message', '')
if (!message) {
message = get(e, 'message')
}
if (message === '') {
if (!message) {
message = 'unknown error'
}

View File

@ -1,23 +1,8 @@
import _ from 'lodash'
// Libraries
import {trimEnd} from 'lodash'
export interface TaskOptions {
name: string
interval: string
cron: string
offset: string
taskScheduleType: TaskSchedule
orgID: string
toOrgName: string
toBucketName: string
}
export type TaskOptionKeys = keyof TaskOptions
export enum TaskSchedule {
interval = 'interval',
cron = 'cron',
unselected = '',
}
// Types
import {TaskOptions, TaskSchedule} from 'src/types'
export const taskOptionsToFluxScript = (options: TaskOptions): string => {
let fluxScript = `option task = { \n name: "${options.name}",\n`
@ -43,7 +28,7 @@ export const addDestinationToFluxScript = (
const {toOrgName, toBucketName} = options
if (toOrgName && toBucketName) {
const trimmedScript = _.trimEnd(script)
const trimmedScript = trimEnd(script)
const trimmedOrgName = toOrgName.trim()
const trimmedBucketName = toBucketName.trim()
return `${trimmedScript}\n |> to(bucket: "${trimmedBucketName}", org: "${trimmedOrgName}")`