Skip to content

Commit

Permalink
Merge pull request #1448 from TheThingsNetwork/feature/1404-console-b…
Browse files Browse the repository at this point in the history
…ulk-create

Bulk Device Creation in Console
  • Loading branch information
kschiffer authored Oct 11, 2019
2 parents 4fab5e7 + 9ba19dc commit f477998
Show file tree
Hide file tree
Showing 21 changed files with 517 additions and 18 deletions.
2 changes: 1 addition & 1 deletion pkg/webui/components/button/button.styl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ $button($color)
background-color: activize($color)
transition-duration: $ad.xs

&:disabled
&:disabled, &.disabled
opacity: .4
cursor: default

Expand Down
13 changes: 12 additions & 1 deletion pkg/webui/components/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function assembleClassnames({
className,
error,
raw,
disabled,
}) {
return classnames(style.button, className, {
[style.danger]: danger,
Expand All @@ -49,6 +50,7 @@ function assembleClassnames({
[style.error]: error && !busy,
[style.large]: large,
[style.raw]: raw,
[style.disabled]: disabled,
})
}

Expand Down Expand Up @@ -99,9 +101,18 @@ Button.defaultProps = {
}

Button.Link = function(props) {
const { disabled, titleMessage } = props
const buttonClassNames = assembleClassnames(props)
const { to } = props
return <Link className={buttonClassNames} to={to} children={buttonChildren(props)} />
return (
<Link
className={buttonClassNames}
to={to}
disabled={disabled}
title={titleMessage}
children={buttonChildren(props)}
/>
)
}
Button.Link.displayName = 'Button.Link'

Expand Down
15 changes: 15 additions & 0 deletions pkg/webui/components/code-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class CodeEditor extends React.Component {
super(props)

this.state = { focus: false }
this.aceRef = React.createRef()
}

onFocus(evt) {
Expand All @@ -53,6 +54,15 @@ class CodeEditor extends React.Component {
})
}

componentDidUpdate({ value }) {
const { value: oldValue, scrollToBottom } = this.props

if (scrollToBottom && value !== oldValue) {
const row = this.aceRef.current.editor.session.getLength()
this.aceRef.current.editor.gotoLine(row)
}
}

render() {
const {
className,
Expand Down Expand Up @@ -117,6 +127,7 @@ class CodeEditor extends React.Component {
onBlur={this.onBlur}
editorProps={{ $blockScrolling: Infinity }}
commands={commands}
ref={this.aceRef}
/>
</div>
)
Expand All @@ -141,6 +152,10 @@ CodeEditor.propTypes = {
editorOptions: PropTypes.object,
/** The height of the editor */
height: PropTypes.string,
/** A flag indicating whether the editor should scroll to the bottom when the
* value has been updated, useful for logging use cases.
*/
scrollToBottom: PropTypes.bool,
/** A flag identifying whether */
showGutter: PropTypes.bool,
/** Minimum lines of code allowed */
Expand Down
1 change: 1 addition & 0 deletions pkg/webui/components/icon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const hardcoded = {
event_update: 'edit',
uplink: 'arrow_drop_up',
downlink: 'arrow_drop_down',
bulk_creation: 'playlist_add',
}

const Icon = function({
Expand Down
5 changes: 4 additions & 1 deletion pkg/webui/components/link/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const formatTitle = function(content, values, formatter) {
const Link = function(props) {
const {
className,
disabled,
title,
titleValues,
id,
Expand All @@ -50,7 +51,9 @@ const Link = function(props) {
return (
<RouterLink
className={
className ? className : classnames(style.link, { [style.linkVisited]: showVisited })
className
? classnames(className, { [style.disabled]: disabled })
: classnames(style.link, { [style.linkVisited]: showVisited, [style.disabled]: disabled })
}
id={id}
title={formattedTitle}
Expand Down
2 changes: 2 additions & 0 deletions pkg/webui/components/link/link.styl
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@
&:visited
color: $tc-deep-gray

.disabled
pointer-events: none
2 changes: 1 addition & 1 deletion pkg/webui/components/progress-bar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default class ProgressBar extends PureComponent {
const displayPercentage = Math.max(0, Math.min(100, percentage)).toFixed(percentageDecimals)
let displayEstimation = null

if (showEstimation) {
if (showEstimation && percentage < 100) {
const now = new Date(Date.now() + 1000)
let eta = new Date(startTime + estimatedDuration)
if (eta < now) {
Expand Down
13 changes: 10 additions & 3 deletions pkg/webui/components/status/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import PropTypes from '../../lib/prop-types'

import style from './status.styl'

const Status = function({ className, status, label, labelValues, children }) {
const Status = function({ className, status, label, pulse, labelValues, children }) {
const cls = classnames(style.status, {
[style.statusGood]: status === 'good',
[style.statusBad]: status === 'bad',
[style.statusMediocre]: status === 'mediocre',
[style.statusUnknown]: status === 'unknown',
[style[`${status}-pulse`]]: pulse,
})

let statusLabel = null
Expand All @@ -48,12 +49,18 @@ const Status = function({ className, status, label, labelValues, children }) {
}

Status.propTypes = {
status: PropTypes.oneOf(['good', 'bad', 'mediocre', 'unknown']),
className: PropTypes.string,
label: PropTypes.message,
labelValues: PropTypes.object,
labelValues: PropTypes.shape({}),
pulse: PropTypes.bool,
status: PropTypes.oneOf(['good', 'bad', 'mediocre', 'unknown']),
}

Status.defaultProps = {
className: undefined,
pulse: true,
label: undefined,
labelValues: undefined,
status: 'unknown',
}

Expand Down
18 changes: 9 additions & 9 deletions pkg/webui/components/status/status.styl
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,27 @@
&-good
background-color: $c-active-blue

&:after
animation: goodPulse 2s infinite

&-bad
background-color: $c-error

&:after
animation: badPulse 2s infinite

&-mediocre
background-color: $c-warning

&:after
animation: mediocrePulse 2s infinite

&-unknown
background-color: $c-icon-fill

&-label
padding-right: $cs.xs

.good-pulse:after
animation: goodPulse 2s infinite

.bad-pulse:after
animation: badPulse 2s infinite

.mediocre-pulse:after
animation: mediocrePulse 2s infinite

pulse-animation($color, $name)
@keyframes $name
0%
Expand Down
8 changes: 8 additions & 0 deletions pkg/webui/components/status/story.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,12 @@ storiesOf('Status', module)
<Status label="Network Status" status="unknown" />
</div>
))
.add('Without Pulse', () => (
<div>
<Status label="No Pulse" status="good" pulse={false} />
<Status label="No Pulse" status="bad" pulse={false} />
<Status label="No Pulse" status="mediocre" pulse={false} />
<Status label="No Pulse" status="unknown" pulse={false} />
</div>
))
.add('Toggle', () => <Toggle />)
79 changes: 79 additions & 0 deletions pkg/webui/console/components/device-bulk-create-form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import React, { Component } from 'react'
import * as Yup from 'yup'
import { defineMessages } from 'react-intl'
import bind from 'autobind-decorator'

import Form from '../../../components/form'
import DeviceTemplateFormatSelect from '../../containers/device-template-format-select'
import FileInput from '../../../components/file-input'
import SubmitBar from '../../../components/submit-bar'
import SubmitButton from '../../../components/submit-button'
import sharedMessages from '../../../lib/shared-messages'

const m = defineMessages({
importFile: 'Import file',
createDevices: 'Create Devices',
selectAFile: 'Please select a template file',
})

const validationSchema = Yup.object({
format_id: Yup.string().required(sharedMessages.validateRequired),
data: Yup.string().required(m.selectAFile),
})

export default class DeviceBulkCreateForm extends Component {
state = {
allowedFileExtensions: undefined,
formatSelected: false,
}

@bind
handleSelectChange(value) {
const newState = { formatSelected: true }
if (value && value.fileExtensions && value.fileExtensions instanceof Array) {
newState.allowedFileExtensions = value.fileExtensions.join(',')
}
this.setState(newState)
}

render() {
const { initialValues, error, onSubmit } = this.props
const { allowedFileExtensions, formatSelected } = this.state
return (
<Form
error={error}
onSubmit={onSubmit}
validationSchema={validationSchema}
submitEnabledWhenInvalid
initialValues={initialValues}
>
<DeviceTemplateFormatSelect onChange={this.handleSelectChange} name="format_id" required />
<Form.Field
disabled={!formatSelected}
title={m.importFile}
accept={allowedFileExtensions}
component={FileInput}
name="data"
required
/>
<SubmitBar>
<Form.Submit component={SubmitButton} message={m.createDevices} />
</SubmitBar>
</Form>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

.title
margin: 0 0 $cs.s 0

.log-output
margin-bottom: $cs.m

.progress-bar
margin-bottom: $cs.m
Loading

0 comments on commit f477998

Please sign in to comment.