Merge pull request #1550 from influxdata/feat/add-import-tasks-page
Import on Tasks Page for flux scriptpull/10616/head
commit
2cac5f30dc
|
@ -15,18 +15,9 @@ interface Props {
|
|||
onImportDashboard: (dashboard: Dashboard) => void
|
||||
notify: (message: Notification) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
isImportable: boolean
|
||||
}
|
||||
|
||||
class ImportDashboardOverlay extends PureComponent<Props, State> {
|
||||
class ImportDashboardOverlay extends PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isImportable: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Notification} from 'src/types'
|
||||
import {TEN_SECONDS} from 'src/shared/constants/index'
|
||||
import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'src/shared/constants/index'
|
||||
|
||||
type NotificationExcludingMessage = Pick<
|
||||
Notification,
|
||||
|
@ -36,3 +36,18 @@ export const taskUpdateFailed = (): Notification => ({
|
|||
...defaultErrorNotification,
|
||||
message: 'Failed to update task',
|
||||
})
|
||||
|
||||
export const taskImportFailed = (
|
||||
fileName: string,
|
||||
errorMessage: string
|
||||
): Notification => ({
|
||||
...defaultErrorNotification,
|
||||
duration: INFINITE,
|
||||
message: `Failed to import Task from file ${fileName}: ${errorMessage}.`,
|
||||
})
|
||||
|
||||
export const taskImportSuccess = (fileName: string): Notification => ({
|
||||
...defaultErrorNotification,
|
||||
duration: FIVE_SECONDS,
|
||||
message: `Successfully imported file ${fileName}.`,
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {AppState} from 'src/types/v2'
|
||||
import {push} from 'react-router-redux'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Task as TaskAPI, Organization} from 'src/api'
|
||||
import {
|
||||
|
@ -18,6 +19,8 @@ import {
|
|||
taskDeleteFailed,
|
||||
taskNotFound,
|
||||
taskUpdateFailed,
|
||||
taskImportFailed,
|
||||
taskImportSuccess,
|
||||
} from 'src/shared/copy/v2/notifications'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
||||
|
@ -309,8 +312,47 @@ export const saveNewScript = () => async (
|
|||
dispatch(setNewScript(''))
|
||||
dispatch(clearTaskOptions())
|
||||
dispatch(goToTasks())
|
||||
dispatch(populateTasks())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
dispatch(notify(taskNotCreated(e.headers['x-influx-error'])))
|
||||
}
|
||||
}
|
||||
|
||||
export const importScript = (script: string, fileName: string) => async (
|
||||
dispatch,
|
||||
getState: GetStateFunc
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const validFileExtension = '.flux'
|
||||
|
||||
const fileExtensionRegex = new RegExp(`${validFileExtension}$`)
|
||||
|
||||
if (!fileName.match(fileExtensionRegex)) {
|
||||
dispatch(notify(taskImportFailed(fileName, 'Please import a .flux file')))
|
||||
return
|
||||
}
|
||||
|
||||
if (_.isEmpty(script)) {
|
||||
dispatch(notify(taskImportFailed(fileName, 'No Flux found in file')))
|
||||
return
|
||||
}
|
||||
|
||||
const {orgs} = await getState()
|
||||
const orgID = getDeep<string>(orgs, '0.id', '') // TODO org selection by user.
|
||||
|
||||
await submitNewTask(orgID, script)
|
||||
|
||||
dispatch(populateTasks())
|
||||
|
||||
dispatch(notify(taskImportSuccess(fileName)))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const explanation = getDeep<string>(
|
||||
error,
|
||||
'response.headers.x-influx-error',
|
||||
''
|
||||
)
|
||||
dispatch(notify(taskImportFailed(fileName, explanation)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// Libraries
|
||||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
|
||||
// Components
|
||||
import ImportTaskOverlay from 'src/tasks/components/ImportTaskOverlay'
|
||||
|
||||
const setup = (override?) => {
|
||||
const props = {
|
||||
onDismissOverlay: oneTestFunction,
|
||||
onSave: oneTestFunction,
|
||||
...override,
|
||||
}
|
||||
|
||||
const wrapper = shallow(<ImportTaskOverlay {...props} />)
|
||||
|
||||
return {wrapper}
|
||||
}
|
||||
const oneTestFunction = jest.fn((script?: string, fileName?: string) => {
|
||||
script = fileName
|
||||
fileName = script
|
||||
return
|
||||
})
|
||||
|
||||
describe('ImportTaskOverlay', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders', () => {
|
||||
const {wrapper} = setup()
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,50 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Container from 'src/clockface/components/overlays/OverlayContainer'
|
||||
import Heading from 'src/clockface/components/overlays/OverlayHeading'
|
||||
import Body from 'src/clockface/components/overlays/OverlayBody'
|
||||
import DragAndDrop from 'src/shared/components/DragAndDrop'
|
||||
|
||||
interface Props {
|
||||
onDismissOverlay: () => void
|
||||
onSave: (script: string, fileName: string) => void
|
||||
}
|
||||
|
||||
class ImportTaskOverlay extends PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {onDismissOverlay} = this.props
|
||||
|
||||
return (
|
||||
<Container maxWidth={800}>
|
||||
<Heading title="Import Task" onDismiss={onDismissOverlay} />
|
||||
<Body>
|
||||
<DragAndDrop
|
||||
submitText="Upload Task"
|
||||
fileTypesToAccept={this.validFileExtension}
|
||||
handleSubmit={this.handleUploadTask}
|
||||
/>
|
||||
</Body>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
private get validFileExtension(): string {
|
||||
return '.flux'
|
||||
}
|
||||
|
||||
private handleUploadTask = (
|
||||
uploadContent: string,
|
||||
fileName: string
|
||||
): void => {
|
||||
const {onSave, onDismissOverlay} = this.props
|
||||
onSave(uploadContent, fileName)
|
||||
onDismissOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
export default ImportTaskOverlay
|
|
@ -23,6 +23,9 @@ interface Task extends TaskAPI {
|
|||
delay?: string
|
||||
}
|
||||
|
||||
// Constants
|
||||
import {IconFont} from 'src/clockface/types/index'
|
||||
|
||||
interface Props {
|
||||
task: Task
|
||||
onActivate: (task: Task) => void
|
||||
|
@ -57,8 +60,8 @@ class TaskRow extends PureComponent<Props & WithRouterProps> {
|
|||
<ComponentSpacer align={Alignment.Right}>
|
||||
<Button
|
||||
size={ComponentSize.ExtraSmall}
|
||||
color={ComponentColor.Default}
|
||||
text="Export"
|
||||
icon={IconFont.Export}
|
||||
onClick={this.handleExport}
|
||||
/>
|
||||
<Button
|
||||
|
|
|
@ -20,6 +20,7 @@ interface Props {
|
|||
setSearchTerm: (searchTerm: string) => void
|
||||
setShowInactive: () => void
|
||||
showInactive: boolean
|
||||
toggleOverlay: () => void
|
||||
}
|
||||
|
||||
export default class TasksHeader extends PureComponent<Props> {
|
||||
|
@ -29,6 +30,7 @@ export default class TasksHeader extends PureComponent<Props> {
|
|||
setSearchTerm,
|
||||
setShowInactive,
|
||||
showInactive,
|
||||
toggleOverlay,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
|
@ -37,7 +39,7 @@ export default class TasksHeader extends PureComponent<Props> {
|
|||
<Page.Title title="Tasks" />
|
||||
</Page.Header.Left>
|
||||
<Page.Header.Right>
|
||||
<label className="tasks-status-toggle">Show Inactive Tasks</label>
|
||||
<label className="tasks-status-toggle">Show Inactive</label>
|
||||
<SlideToggle
|
||||
active={showInactive}
|
||||
size={ComponentSize.ExtraSmall}
|
||||
|
@ -48,6 +50,11 @@ export default class TasksHeader extends PureComponent<Props> {
|
|||
onSearch={setSearchTerm}
|
||||
/>
|
||||
<TaskOrgDropdown />
|
||||
<Button
|
||||
text="Import"
|
||||
icon={IconFont.Import}
|
||||
onClick={toggleOverlay}
|
||||
/>
|
||||
<Button
|
||||
color={ComponentColor.Primary}
|
||||
onClick={onCreateTask}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImportTaskOverlay rendering renders 1`] = `
|
||||
<OverlayContainer
|
||||
maxWidth={800}
|
||||
>
|
||||
<OverlayHeading
|
||||
onDismiss={[MockFunction]}
|
||||
title="Import Task"
|
||||
/>
|
||||
<OverlayBody>
|
||||
<DragAndDrop
|
||||
compact={false}
|
||||
fileTypesToAccept=".flux"
|
||||
handleSubmit={[Function]}
|
||||
submitOnDrop={false}
|
||||
submitOnUpload={false}
|
||||
submitText="Upload Task"
|
||||
/>
|
||||
</OverlayBody>
|
||||
</OverlayContainer>
|
||||
`;
|
|
@ -0,0 +1,47 @@
|
|||
// Libraries
|
||||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
|
||||
// Components
|
||||
import TasksList from 'src/tasks/components/TasksList'
|
||||
|
||||
// Types
|
||||
import {Task} from 'src/types/v2/tasks'
|
||||
|
||||
// Constants
|
||||
import {tasks} from 'mocks/dummyData'
|
||||
|
||||
const setup = (override?) => {
|
||||
const props = {
|
||||
tasks,
|
||||
searchTerm: '',
|
||||
onActivate: oneTestFunction,
|
||||
onDelete: oneTestFunction,
|
||||
onCreate: secondTestFunction,
|
||||
onSelect: oneTestFunction,
|
||||
...override,
|
||||
}
|
||||
|
||||
const wrapper = shallow(<TasksList {...props} />)
|
||||
|
||||
return {wrapper}
|
||||
}
|
||||
|
||||
const oneTestFunction = (tasks: Task) => {
|
||||
tasks[0].name = 'someone'
|
||||
return
|
||||
}
|
||||
|
||||
const secondTestFunction = () => {
|
||||
return
|
||||
}
|
||||
|
||||
describe('TasksList', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders', () => {
|
||||
const {wrapper} = setup()
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -8,6 +8,8 @@ import TasksHeader from 'src/tasks/components/TasksHeader'
|
|||
import TasksList from 'src/tasks/components/TasksList'
|
||||
import {Page} from 'src/pageLayout'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {OverlayTechnology} from 'src/clockface'
|
||||
import ImportTaskOverlay from 'src/tasks/components/ImportTaskOverlay'
|
||||
|
||||
// Actions
|
||||
import {
|
||||
|
@ -18,6 +20,7 @@ import {
|
|||
setSearchTerm as setSearchTermAction,
|
||||
setShowInactive as setShowInactiveAction,
|
||||
setDropdownOrgID as setDropdownOrgIDAction,
|
||||
importScript,
|
||||
} from 'src/tasks/actions/v2'
|
||||
|
||||
// Constants
|
||||
|
@ -44,6 +47,7 @@ interface ConnectedDispatchProps {
|
|||
setSearchTerm: typeof setSearchTermAction
|
||||
setShowInactive: typeof setShowInactiveAction
|
||||
setDropdownOrgID: typeof setDropdownOrgIDAction
|
||||
importScript: typeof importScript
|
||||
}
|
||||
|
||||
interface ConnectedStateProps {
|
||||
|
@ -56,8 +60,12 @@ interface ConnectedStateProps {
|
|||
|
||||
type Props = ConnectedDispatchProps & PassedInProps & ConnectedStateProps
|
||||
|
||||
interface State {
|
||||
isOverlayVisible: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TasksPage extends PureComponent<Props> {
|
||||
class TasksPage extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
|
@ -66,6 +74,8 @@ class TasksPage extends PureComponent<Props> {
|
|||
props.setShowInactive()
|
||||
}
|
||||
props.setDropdownOrgID(null)
|
||||
|
||||
this.state = {isOverlayVisible: false}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
|
@ -75,27 +85,32 @@ class TasksPage extends PureComponent<Props> {
|
|||
setShowInactive,
|
||||
showInactive,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<TasksHeader
|
||||
onCreateTask={this.handleCreateTask}
|
||||
setSearchTerm={setSearchTerm}
|
||||
setShowInactive={setShowInactive}
|
||||
showInactive={showInactive}
|
||||
/>
|
||||
<Page.Contents fullWidth={false} scrollable={true}>
|
||||
<div className="col-xs-12">
|
||||
<TasksList
|
||||
searchTerm={searchTerm}
|
||||
tasks={this.filteredTasks}
|
||||
onActivate={this.handleActivate}
|
||||
onDelete={this.handleDelete}
|
||||
onCreate={this.handleCreateTask}
|
||||
onSelect={this.props.selectTask}
|
||||
/>
|
||||
</div>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
<>
|
||||
<Page>
|
||||
<TasksHeader
|
||||
onCreateTask={this.handleCreateTask}
|
||||
setSearchTerm={setSearchTerm}
|
||||
setShowInactive={setShowInactive}
|
||||
showInactive={showInactive}
|
||||
toggleOverlay={this.handleToggleOverlay}
|
||||
/>
|
||||
<Page.Contents fullWidth={false} scrollable={true}>
|
||||
<div className="col-xs-12">
|
||||
<TasksList
|
||||
searchTerm={searchTerm}
|
||||
tasks={this.filteredTasks}
|
||||
onActivate={this.handleActivate}
|
||||
onDelete={this.handleDelete}
|
||||
onCreate={this.handleCreateTask}
|
||||
onSelect={this.props.selectTask}
|
||||
/>
|
||||
</div>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
{this.renderImportOverlay}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -116,6 +131,27 @@ class TasksPage extends PureComponent<Props> {
|
|||
router.push('/tasks/new')
|
||||
}
|
||||
|
||||
private handleToggleOverlay = () => {
|
||||
this.setState({isOverlayVisible: !this.state.isOverlayVisible})
|
||||
}
|
||||
|
||||
private handleSave = (script: string, fileName: string) => {
|
||||
this.props.importScript(script, fileName)
|
||||
}
|
||||
|
||||
private get renderImportOverlay(): JSX.Element {
|
||||
const {isOverlayVisible} = this.state
|
||||
|
||||
return (
|
||||
<OverlayTechnology visible={isOverlayVisible}>
|
||||
<ImportTaskOverlay
|
||||
onDismissOverlay={this.handleToggleOverlay}
|
||||
onSave={this.handleSave}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
)
|
||||
}
|
||||
|
||||
private get filteredTasks(): Task[] {
|
||||
const {tasks, searchTerm, showInactive, dropdownOrgID} = this.props
|
||||
const matchingTasks = tasks.filter(t => {
|
||||
|
@ -158,6 +194,7 @@ const mdtp: ConnectedDispatchProps = {
|
|||
setSearchTerm: setSearchTermAction,
|
||||
setShowInactive: setShowInactiveAction,
|
||||
setDropdownOrgID: setDropdownOrgIDAction,
|
||||
importScript,
|
||||
}
|
||||
|
||||
export default connect<
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TasksList rendering renders 1`] = `
|
||||
<IndexList>
|
||||
<IndexListHeader>
|
||||
<IndexListHeaderCell
|
||||
alignment="left"
|
||||
columnName="name"
|
||||
onClick={[Function]}
|
||||
sort="none"
|
||||
sortKey="name"
|
||||
width="20%"
|
||||
/>
|
||||
<IndexListHeaderCell
|
||||
alignment="left"
|
||||
columnName="active"
|
||||
onClick={[Function]}
|
||||
sort="none"
|
||||
sortKey="status"
|
||||
width="10%"
|
||||
/>
|
||||
<IndexListHeaderCell
|
||||
alignment="left"
|
||||
columnName="schedule"
|
||||
onClick={[Function]}
|
||||
sort="none"
|
||||
sortKey="every"
|
||||
width="20%"
|
||||
/>
|
||||
<IndexListHeaderCell
|
||||
alignment="left"
|
||||
columnName="organization"
|
||||
onClick={[Function]}
|
||||
sort="none"
|
||||
sortKey="organization.name"
|
||||
width="15%"
|
||||
/>
|
||||
<IndexListHeaderCell
|
||||
alignment="left"
|
||||
columnName=""
|
||||
width="35%"
|
||||
/>
|
||||
</IndexListHeader>
|
||||
<IndexListBody
|
||||
columnCount={5}
|
||||
emptyState={
|
||||
<EmptyTasksLists
|
||||
onCreate={[Function]}
|
||||
searchTerm=""
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SortingHat
|
||||
direction="desc"
|
||||
list={
|
||||
Array [
|
||||
Object {
|
||||
"cron": "2 0 * * *",
|
||||
"flux": "option task = {
|
||||
name: \\"pasdlak\\",
|
||||
cron: \\"2 0 * * *\\"
|
||||
}
|
||||
from(bucket: \\"inbucket\\")
|
||||
|> range(start: -1h)",
|
||||
"id": "02ef9deff2141000",
|
||||
"name": "pasdlak",
|
||||
"organization": Object {
|
||||
"id": "02ee9e2a29d73000",
|
||||
"links": Object {
|
||||
"buckets": "/api/v2/buckets?org=RadicalOrganization",
|
||||
"dashboards": "/api/v2/dashboards?org=RadicalOrganization",
|
||||
"log": "/api/v2/orgs/02ee9e2a29d73000/log",
|
||||
"members": "/api/v2/orgs/02ee9e2a29d73000/members",
|
||||
"self": "/api/v2/orgs/02ee9e2a29d73000",
|
||||
"tasks": "/api/v2/tasks?org=RadicalOrganization",
|
||||
},
|
||||
"name": "RadicalOrganization",
|
||||
},
|
||||
"organizationId": "02ee9e2a29d73000",
|
||||
"owner": Object {
|
||||
"id": "02ee9e2a19d73000",
|
||||
"name": "",
|
||||
},
|
||||
"status": "active",
|
||||
},
|
||||
Object {
|
||||
"every": "1m0s",
|
||||
"flux": "option task = {
|
||||
name: \\"somename\\",
|
||||
every: 1m,
|
||||
}
|
||||
from(bucket: \\"inbucket\\")
|
||||
|> range(start: -task.every)",
|
||||
"id": "02f12c50dba72000",
|
||||
"name": "somename",
|
||||
"organization": Object {
|
||||
"id": "02ee9e2a29d73000",
|
||||
"links": Object {
|
||||
"buckets": "/api/v2/buckets?org=RadicalOrganization",
|
||||
"dashboards": "/api/v2/dashboards?org=RadicalOrganization",
|
||||
"log": "/api/v2/orgs/02ee9e2a29d73000/log",
|
||||
"members": "/api/v2/orgs/02ee9e2a29d73000/members",
|
||||
"self": "/api/v2/orgs/02ee9e2a29d73000",
|
||||
"tasks": "/api/v2/tasks?org=RadicalOrganization",
|
||||
},
|
||||
"name": "RadicalOrganization",
|
||||
},
|
||||
"organizationId": "02ee9e2a29d73000",
|
||||
"owner": Object {
|
||||
"id": "02ee9e2a19d73000",
|
||||
"name": "",
|
||||
},
|
||||
"status": "active",
|
||||
},
|
||||
]
|
||||
}
|
||||
sortKey={null}
|
||||
>
|
||||
<Component />
|
||||
</SortingHat>
|
||||
</IndexListBody>
|
||||
</IndexList>
|
||||
`;
|
Loading…
Reference in New Issue