Merge pull request #1550 from influxdata/feat/add-import-tasks-page

Import on Tasks Page for flux script
pull/10616/head
Palakp41 2018-11-21 14:42:00 -08:00 committed by GitHub
commit 2cac5f30dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 404 additions and 34 deletions

View File

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

View File

@ -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}.`,
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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