diff --git a/pkg/webui/components/events/index.js b/pkg/webui/components/events/index.js index 24c1b49c6d..3b2598eb8c 100644 --- a/pkg/webui/components/events/index.js +++ b/pkg/webui/components/events/index.js @@ -34,7 +34,45 @@ const m = defineMessages({ }) @bind -class Events extends React.PureComponent { +class Events extends React.Component { + + state = { + paused: false, + } + + shouldComponentUpdate (nextProps, nextState) { + const { paused } = this.state + const { + events, + emitterId, + onClear, + limit, + } = this.props + + if ( + emitterId !== nextProps.emitterId + || limit !== nextProps.limit + || onClear !== nextProps.onClear + || paused !== nextState.paused + ) { + return true + } + + const newEvents = events !== nextProps.events + const clearedEvents = newEvents && nextProps.events.length === 0 + + // rerender component if cleared events when in the `paused` state + if (clearedEvents && paused) { + return true + } + + // do not rerender component on new events when in the `paused` state + if (newEvents && paused) { + return false + } + + return newEvents + } renderEvent (event) { const { component: Component, type } = getEventComponentByName(event.name) @@ -52,9 +90,7 @@ class Events extends React.PureComponent { } onPause () { - const { onPause } = this.props - - onPause() + this.setState(prev => ({ paused: !prev.paused })) } onClear () { @@ -71,12 +107,11 @@ class Events extends React.PureComponent { const { className, events, - paused, onClear, - onPause, emitterId, limit, } = this.props + const { paused } = this.state let limitedEvents = events const truncated = events.length > limit @@ -87,7 +122,7 @@ class Events extends React.PureComponent { const header = ( ) @@ -116,9 +151,7 @@ class Events extends React.PureComponent { Events.propTypes = { events: PropTypes.arrayOf(PropTypes.event), - paused: PropTypes.bool.isRequired, emitterId: PropTypes.string.isRequired, - onPause: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired, limit: PropTypes.number, } diff --git a/pkg/webui/components/events/widget/index.js b/pkg/webui/components/events/widget/index.js index e77a65209e..4c07d6d37a 100644 --- a/pkg/webui/components/events/widget/index.js +++ b/pkg/webui/components/events/widget/index.js @@ -28,7 +28,7 @@ import sharedMessages from '../../../lib/shared-messages' import style from './widget.styl' const m = defineMessages({ - latestEvents: 'Latest events', + latestEvents: 'Latest Events', seeAllActivity: 'See all activity', unknown: 'Unknown', }) @@ -133,4 +133,13 @@ EventsWidget.defaultProps = { limit: 5, } +const CONNECTION_STATUS = Object.freeze({ + GOOD: 'good', + BAD: 'bad', + MEDIOCRE: 'mediocre', + UNKNOWN: 'unknown', +}) + +EventsWidget.CONNECTION_STATUS = CONNECTION_STATUS + export default EventsWidget diff --git a/pkg/webui/components/events/widget/widget.styl b/pkg/webui/components/events/widget/widget.styl index 80fece05b1..9fad96a0db 100644 --- a/pkg/webui/components/events/widget/widget.styl +++ b/pkg/webui/components/events/widget/widget.styl @@ -25,4 +25,5 @@ min-height: 10rem .status-message + font-weight: 600 margin-right: $cs.xs diff --git a/pkg/webui/console/api/index.js b/pkg/webui/console/api/index.js index a743a0de40..166b17e26a 100644 --- a/pkg/webui/console/api/index.js +++ b/pkg/webui/console/api/index.js @@ -77,6 +77,7 @@ export default { 'delete': ttnClient.Applications.deleteById.bind(ttnClient.Applications), create: ttnClient.Applications.create.bind(ttnClient.Applications), update: ttnClient.Applications.updateById.bind(ttnClient.Applications), + eventsSubscribe: ttnClient.Applications.openStream.bind(ttnClient.Applications), apiKeys: { list: ttnClient.Applications.ApiKeys.getAll.bind(ttnClient.Applications.ApiKeys), update: ttnClient.Applications.ApiKeys.updateById.bind(ttnClient.Applications.ApiKeys), diff --git a/pkg/webui/console/constants/connection-status.js b/pkg/webui/console/constants/connection-status.js new file mode 100644 index 0000000000..2c9e1e8b17 --- /dev/null +++ b/pkg/webui/console/constants/connection-status.js @@ -0,0 +1,20 @@ +// 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. + +export default Object.freeze({ + CONNECTED: 'connected', + DISCONNECTED: 'disconnected', + CONNECTING: 'connecting', + UNKNOWN: 'unknown', +}) diff --git a/pkg/webui/console/containers/application-events/index.js b/pkg/webui/console/containers/application-events/index.js new file mode 100644 index 0000000000..0b16e22a8c --- /dev/null +++ b/pkg/webui/console/containers/application-events/index.js @@ -0,0 +1,67 @@ +// 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 from 'react' +import { connect } from 'react-redux' +import bind from 'autobind-decorator' + +import PropTypes from '../../../lib/prop-types' +import EventsSubscription from '../../containers/events-subscription' + +import { + clearApplicationEventsStream, +} from '../../store/actions/application' + +import { + applicationEventsSelector, + applicationEventsStatusSelector, +} from '../../store/selectors/application' + +@connect( + null, + (dispatch, ownProps) => ({ + onClear: () => dispatch(clearApplicationEventsStream(ownProps.appId)), + })) +@bind +class ApplicationEvents extends React.Component { + render () { + const { + appId, + widget, + onClear, + } = this.props + + return ( + + ) + } +} + +ApplicationEvents.propTypes = { + appId: PropTypes.string.isRequired, + widget: PropTypes.bool, +} + +ApplicationEvents.defaultProps = { + widget: false, +} + +export default ApplicationEvents diff --git a/pkg/webui/console/containers/events-subscription/index.js b/pkg/webui/console/containers/events-subscription/index.js new file mode 100644 index 0000000000..98bd3899fb --- /dev/null +++ b/pkg/webui/console/containers/events-subscription/index.js @@ -0,0 +1,97 @@ +// 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 from 'react' +import { connect } from 'react-redux' + +import CONNECTION_STATUS from '../../constants/connection-status' +import Events from '../../../components/events' +import PropTypes from '../../../lib/prop-types' + +const { Widget } = Events + +const mapConnectionStatusToWidget = function (status) { + switch (status) { + case CONNECTION_STATUS.CONNECTED: + return Widget.CONNECTION_STATUS.GOOD + case CONNECTION_STATUS.CONNECTING: + return Widget.CONNECTION_STATUS.MEDIOCRE + case CONNECTION_STATUS.DISCONNECTED: + return Widget.CONNECTION_STATUS.BAD + case CONNECTION_STATUS.UNKNOWN: + default: + return Widget.CONNECTION_STATUS.UNKNOWN + } +} + +@connect(function (state, props) { + const { + eventsSelector, + statusSelector, + } = props + + return { + events: eventsSelector(state, props), + connectionStatus: statusSelector(state, props), + } +}) +class EventsSubscription extends React.Component { + render () { + const { + id, + widget, + events, + connectionStatus, + onClear, + toAllUrl, + } = this.props + + if (widget) { + return ( + + ) + } + + return ( + + ) + } +} + +EventsSubscription.propTypes = { + id: PropTypes.string.isRequired, + eventsSelector: PropTypes.func.isRequired, + statusSelector: PropTypes.func, + onClear: PropTypes.func, + widget: PropTypes.bool, + toAllUrl: PropTypes.string, +} + +EventsSubscription.defaultProps = { + widget: false, + onClear: () => null, + statusSelector: () => 'unknown', + toAllUrl: null, +} + +export default EventsSubscription diff --git a/pkg/webui/console/store/actions/application.js b/pkg/webui/console/store/actions/application.js index e0a3f978e4..39ad2a1e10 100644 --- a/pkg/webui/console/store/actions/application.js +++ b/pkg/webui/console/store/actions/application.js @@ -34,6 +34,19 @@ import { getCollaborator, } from '../actions/collaborators' +import { + startEventsStream, + createStartEventsStreamActionType, + startEventsStreamSuccess, + createStartEventsStreamSuccessActionType, + startEventsStreamFailure, + createStartEventsStreamFailureActionType, + stopEventsStream, + createStopEventsStreamActionType, + clearEvents, + createClearEventsActionType, +} from '../actions/events' + export const SHARED_NAME = 'APPLICATION' export const GET_APP = 'GET_APPLICATION' @@ -47,6 +60,11 @@ export const GET_APP_COLLABORATOR_PAGE_DATA = createGetCollaboratorActionType(SH export const GET_APP_COLLABORATORS_LIST = createGetCollaboratorsListActionType(SHARED_NAME) export const GET_APP_COLLABORATORS_LIST_SUCCESS = createGetCollaboratorsListSuccessActionType(SHARED_NAME) export const GET_APP_COLLABORATORS_LIST_FAILURE = createGetCollaboratorsListFailureActionType(SHARED_NAME) +export const START_APP_EVENT_STREAM = createStartEventsStreamActionType(SHARED_NAME) +export const START_APP_EVENT_STREAM_SUCCESS = createStartEventsStreamSuccessActionType(SHARED_NAME) +export const START_APP_EVENT_STREAM_FAILURE = createStartEventsStreamFailureActionType(SHARED_NAME) +export const STOP_APP_EVENT_STREAM = createStopEventsStreamActionType(SHARED_NAME) +export const CLEAR_APP_EVENTS = createClearEventsActionType(SHARED_NAME) export const getApplication = id => ( { type: GET_APP, id } @@ -75,3 +93,13 @@ export const getApplicationCollaboratorsListSuccess = getCollaboratorsListSucces export const getApplicationCollaboratorsListFailure = getCollaboratorsListFailure(SHARED_NAME) export const getApplicationCollaboratorPageData = getCollaborator(SHARED_NAME) + +export const startApplicationEventsStream = startEventsStream(SHARED_NAME) + +export const startApplicationEventsStreamSuccess = startEventsStreamSuccess(SHARED_NAME) + +export const startApplicationEventsStreamFailure = startEventsStreamFailure(SHARED_NAME) + +export const stopApplicationEventsStream = stopEventsStream(SHARED_NAME) + +export const clearApplicationEventsStream = clearEvents(SHARED_NAME) diff --git a/pkg/webui/console/store/actions/events.js b/pkg/webui/console/store/actions/events.js new file mode 100644 index 0000000000..be545c3789 --- /dev/null +++ b/pkg/webui/console/store/actions/events.js @@ -0,0 +1,69 @@ +// 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. + +export const createStartEventsStreamActionType = name => ( + `START_${name}_EVENT_STREAM` +) + +export const createStartEventsStreamSuccessActionType = name => ( + `START_${name}_EVENT_STREAM_SUCCESS` +) + +export const createStartEventsStreamFailureActionType = name => ( + `START_${name}_EVENT_STREAM_FAILURE` +) + +export const createStopEventsStreamActionType = name => ( + `STOP_${name}_EVENT_STREAM` +) + +export const createGetEventMessageSuccessActionType = name => ( + `GET_${name}_EVENT_MESSAGE_SUCCESS` +) + +export const createGetEventMessageFailureActionType = name => ( + `GET_${name}_EVENT_MESSAGE_FAILURE` +) + +export const createClearEventsActionType = name => ( + `CLEAR_${name}_EVENTS` +) + +export const startEventsStream = name => id => ( + { type: createStartEventsStreamActionType(name), id } +) + +export const startEventsStreamSuccess = name => id => ( + { type: createStartEventsStreamSuccessActionType(name), id } +) + +export const startEventsStreamFailure = name => (id, error) => ( + { type: createStartEventsStreamFailureActionType(name), id, error } +) + +export const stopEventsStream = name => id => ( + { type: createStopEventsStreamActionType(name), id } +) + +export const getEventMessageSuccess = name => (id, event) => ( + { type: createGetEventMessageSuccessActionType(name), id, event } +) + +export const getEventMessageFailure = name => (id, error) => ( + { type: createGetEventMessageFailureActionType(name), id, error } +) + +export const clearEvents = name => id => ( + { type: createClearEventsActionType(name), id } +) diff --git a/pkg/webui/console/store/middleware/application.js b/pkg/webui/console/store/middleware/application.js index a5a797f05d..8048f33930 100644 --- a/pkg/webui/console/store/middleware/application.js +++ b/pkg/webui/console/store/middleware/application.js @@ -16,6 +16,7 @@ import { createLogic } from 'redux-logic' import api from '../../api' import * as application from '../actions/application' +import createEventsConnectLogic from './events' const getApplicationLogic = createLogic({ type: [ application.GET_APP ], @@ -98,4 +99,5 @@ export default [ getApplicationLogic, getApplicationApiKeysLogic, getApplicationCollaboratorsLogic, + ...createEventsConnectLogic(application.SHARED_NAME, 'application'), ] diff --git a/pkg/webui/console/store/middleware/events.js b/pkg/webui/console/store/middleware/events.js new file mode 100644 index 0000000000..5ecbe8c41b --- /dev/null +++ b/pkg/webui/console/store/middleware/events.js @@ -0,0 +1,107 @@ +// 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 { createLogic } from 'redux-logic' + +import CONNECTION_STATUS from '../../constants/connection-status' +import api from '../../api' +import { + createStartEventsStreamActionType, + createStopEventsStreamActionType, + createStartEventsStreamFailureActionType, + getEventMessageSuccess, + getEventMessageFailure, + startEventsStreamFailure, + startEventsStreamSuccess, + stopEventsStream, +} from '../actions/events' +import { + applicationEventsStatusSelector, +} from '../selectors/application' + +const createEventsConnectLogic = function (name, entity) { + const START_EVENTS = createStartEventsStreamActionType(name) + const START_EVENTS_FAILURE = createStartEventsStreamFailureActionType(name) + const STOP_EVENTS = createStopEventsStreamActionType(name) + const startEventsSuccess = startEventsStreamSuccess(name) + const startEventsFailure = startEventsStreamFailure(name) + const stopEvents = stopEventsStream(name) + const getEventSuccess = getEventMessageSuccess(name) + const getEventFailure = getEventMessageFailure(name) + + let channel = null + + return [ + createLogic({ + type: START_EVENTS, + warnTimeout: 0, + validate ({ getState, action }, allow, reject) { + const { id } = action + if (!id) { + reject() + } + + // only proceed if not already connected + const status = applicationEventsStatusSelector(getState(), { id }) + const connected = status === CONNECTION_STATUS.CONNECTED + const connecting = status === CONNECTION_STATUS.CONNECTING + if (connected || connecting) { + reject() + } + + allow(action) + }, + async process ({ action }, dispatch, done) { + const { eventsSubscribe } = api[entity] + const { id } = action + + try { + channel = await eventsSubscribe([ id ]) + channel.on('start', () => dispatch(startEventsSuccess(id))) + channel.on('event', message => dispatch(getEventSuccess(id, message))) + channel.on('error', error => dispatch(getEventFailure(id, error))) + channel.on('close', () => dispatch(stopEvents(id))) + } catch (error) { + dispatch(startEventsFailure(error)) + done() + } + }, + }), + createLogic({ + type: [ STOP_EVENTS, START_EVENTS_FAILURE ], + validate ({ getState, action }, allow, reject) { + const { id } = action + if (!id || !channel) { + reject() + } + + // only proceed if connected + const status = applicationEventsStatusSelector(getState(), { id }) + const disconnected = status === CONNECTION_STATUS.DISCONNECTED + const unknown = status === CONNECTION_STATUS.UNKNOWN + if (disconnected || unknown) { + reject() + } + + allow(action) + }, + process (helpers, dispatch, done) { + channel.close() + done() + }, + }), + ] +} + +export default createEventsConnectLogic diff --git a/pkg/webui/console/store/reducers/events.js b/pkg/webui/console/store/reducers/events.js new file mode 100644 index 0000000000..f8ede6f03c --- /dev/null +++ b/pkg/webui/console/store/reducers/events.js @@ -0,0 +1,114 @@ +// 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 CONNECTION_STATUS from '../../constants/connection-status' +import { + createGetEventMessageSuccessActionType, + createGetEventMessageFailureActionType, + createStartEventsStreamActionType, + createStartEventsStreamSuccessActionType, + createStartEventsStreamFailureActionType, + createStopEventsStreamActionType, + createClearEventsActionType, +} from '../actions/events' + +const defaultState = { + events: [], + error: false, + status: CONNECTION_STATUS.DISCONNECTED, +} + +const createNamedEventReducer = function (reducerName = '') { + const START_EVENTS = createStartEventsStreamActionType(reducerName) + const START_EVENTS_SUCCESS = createStartEventsStreamSuccessActionType(reducerName) + const START_EVENTS_FAILURE = createStartEventsStreamFailureActionType(reducerName) + const STOP_EVENTS = createStopEventsStreamActionType(reducerName) + const GET_EVENT_SUCCESS = createGetEventMessageSuccessActionType(reducerName) + const GET_EVENT_FAILURE = createGetEventMessageFailureActionType(reducerName) + const CLEAR_EVENTS = createClearEventsActionType(reducerName) + + return function (state = defaultState, action) { + switch (action.type) { + case START_EVENTS: + return { + ...state, + status: CONNECTION_STATUS.CONNECTING, + } + case START_EVENTS_SUCCESS: + return { + ...state, + status: CONNECTION_STATUS.CONNECTED, + } + case GET_EVENT_SUCCESS: + return { + ...state, + events: [ action.event, ...state.events ], + } + case START_EVENTS_FAILURE: + case GET_EVENT_FAILURE: + return { + ...state, + error: action.error, + state: CONNECTION_STATUS.UNKNOWN, + } + case STOP_EVENTS: + return { + ...state, + status: CONNECTION_STATUS.DISCONNECTED, + } + case CLEAR_EVENTS: + return { + ...state, + events: [], + } + default: + return state + } + } +} + +const createNamedEventsReducer = function (reducerName = '') { + const START_EVENTS = createStartEventsStreamActionType(reducerName) + const START_EVENTS_SUCCESS = createStartEventsStreamSuccessActionType(reducerName) + const START_EVENTS_FAILURE = createStartEventsStreamFailureActionType(reducerName) + const GET_EVENT_SUCCESS = createGetEventMessageSuccessActionType(reducerName) + const GET_EVENT_FAILURE = createGetEventMessageFailureActionType(reducerName) + const CLEAR_EVENTS = createClearEventsActionType(reducerName) + const STOP_EVENTS = createStopEventsStreamActionType(reducerName) + const event = createNamedEventReducer(reducerName) + + return function (state = {}, action) { + if (!action.id) { + return state + } + + switch (action.type) { + case START_EVENTS: + case START_EVENTS_FAILURE: + case START_EVENTS_SUCCESS: + case STOP_EVENTS: + case GET_EVENT_FAILURE: + case GET_EVENT_SUCCESS: + case CLEAR_EVENTS: + return { + ...state, + [action.id]: event(state[action.id], action), + } + default: + return state + } + } +} + +export default createNamedEventsReducer diff --git a/pkg/webui/console/store/reducers/index.js b/pkg/webui/console/store/reducers/index.js index f783434fdf..544df81733 100644 --- a/pkg/webui/console/store/reducers/index.js +++ b/pkg/webui/console/store/reducers/index.js @@ -27,6 +27,7 @@ import configuration from './configuration' import createNamedApiKeysReducer from './api-keys' import createNamedRightsReducer from './rights' import createNamedCollaboratorsReducer from './collaborators' +import createNamedEventsReducer from './events' export default combineReducers({ user, @@ -47,4 +48,7 @@ export default combineReducers({ collaborators: combineReducers({ applications: createNamedCollaboratorsReducer(APPLICATION_SHARED_NAME), }), + events: combineReducers({ + applications: createNamedEventsReducer(APPLICATION_SHARED_NAME), + }), }) diff --git a/pkg/webui/console/store/selectors/application.js b/pkg/webui/console/store/selectors/application.js new file mode 100644 index 0000000000..8caaf9888e --- /dev/null +++ b/pkg/webui/console/store/selectors/application.js @@ -0,0 +1,27 @@ +// 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 { + eventsSelector, + errorSelector as eventsErrorSelector, + statusSelector as eventsStatusSelector, +} from './events' + +const ENTITY = 'applications' + +export const applicationEventsSelector = eventsSelector(ENTITY) + +export const applicationEventsErrorSelector = eventsErrorSelector(ENTITY) + +export const applicationEventsStatusSelector = eventsStatusSelector(ENTITY) diff --git a/pkg/webui/console/store/selectors/events.js b/pkg/webui/console/store/selectors/events.js new file mode 100644 index 0000000000..ad54e4bd11 --- /dev/null +++ b/pkg/webui/console/store/selectors/events.js @@ -0,0 +1,33 @@ +// 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. + +const storeSelector = (state, props) => state[props.id] + +export const eventsSelector = entity => function (state, props) { + const store = storeSelector(state.events[entity], props) + + return store ? store.events : [] +} + +export const statusSelector = entity => function (state, props) { + const store = storeSelector(state.events[entity], props) + + return store ? store.status : 'unknown' +} + +export const errorSelector = entity => function (state, props) { + const store = storeSelector(state.events[entity], props) + + return store ? store.error : false +} diff --git a/pkg/webui/console/views/application-data/application-data.styl b/pkg/webui/console/views/application-data/application-data.styl new file mode 100644 index 0000000000..f58a61bbf8 --- /dev/null +++ b/pkg/webui/console/views/application-data/application-data.styl @@ -0,0 +1,16 @@ +// 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. + +.wrapper + margin-bottom: $ls.xxs diff --git a/pkg/webui/console/views/application-data/index.js b/pkg/webui/console/views/application-data/index.js new file mode 100644 index 0000000000..cd52249719 --- /dev/null +++ b/pkg/webui/console/views/application-data/index.js @@ -0,0 +1,64 @@ +// 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 from 'react' +import { defineMessages } from 'react-intl' +import { Container, Col, Row } from 'react-grid-system' + +import IntlHelmet from '../../../lib/components/intl-helmet' +import sharedMessages from '../../../lib/shared-messages' +import Breadcrumb from '../../../components/breadcrumbs/breadcrumb' +import { withBreadcrumb } from '../../../components/breadcrumbs/context' +import Message from '../../../lib/components/message' +import ApplicationEvents from '../../containers/application-events' + +import style from './application-data.styl' + +const m = defineMessages({ + appData: 'Application Data', +}) + +@withBreadcrumb('apps.single.data', function (props) { + return ( + + ) +}) +export default class Data extends React.Component { + + render () { + const { appId } = this.props.match.params + + return ( + + + + + + + + + + + + + + ) + } +} diff --git a/pkg/webui/console/views/application-overview/application-overview.styl b/pkg/webui/console/views/application-overview/application-overview.styl index 1927b9d0aa..06c1165d0d 100644 --- a/pkg/webui/console/views/application-overview/application-overview.styl +++ b/pkg/webui/console/views/application-overview/application-overview.styl @@ -46,21 +46,3 @@ .latest-events margin-top: $ls.m - - h4 - one-liner(block) - height: 1rem - margin: 0 - - div - border-normal() - box-sizing: border-box - display: flex - align-items: center - justify-content: center - width: 100% - height: 12rem - margin-top: $ls.xxs - text-align: center - font-style: italic - color: $tc-subtle-gray diff --git a/pkg/webui/console/views/application-overview/index.js b/pkg/webui/console/views/application-overview/index.js index 44a5c860d9..a934d114f3 100644 --- a/pkg/webui/console/views/application-overview/index.js +++ b/pkg/webui/console/views/application-overview/index.js @@ -20,7 +20,7 @@ import sharedMessages from '../../../lib/shared-messages' import DateTime from '../../../lib/components/date-time' import DevicesTable from '../../containers/devices-table' import DataSheet from '../../../components/data-sheet' - +import ApplicationEvents from '../../containers/application-events' import style from './application-overview.styl' @@ -65,6 +65,8 @@ class ApplicationOverview extends React.Component { } render () { + const { appId } = this.props.match.params + return ( @@ -73,8 +75,10 @@ class ApplicationOverview extends React.Component { - Latest Data - Activity Events Placeholder + diff --git a/pkg/webui/console/views/application/index.js b/pkg/webui/console/views/application/index.js index 3a0912c0a3..6735d22216 100644 --- a/pkg/webui/console/views/application/index.js +++ b/pkg/webui/console/views/application/index.js @@ -28,8 +28,13 @@ import ApplicationGeneralSettings from '../application-general-settings' import ApplicationApiKeys from '../application-api-keys' import ApplicationLink from '../application-link' import ApplicationCollaborators from '../application-collaborators' +import ApplicationData from '../application-data' -import { getApplication } from '../../store/actions/application' +import { + getApplication, + startApplicationEventsStream, + stopApplicationEventsStream, +} from '../../store/actions/application' import Devices from '../devices' @@ -40,7 +45,12 @@ import Devices from '../devices' application: application.application, error: application.error, } -}) +}, +dispatch => ({ + startStream: id => dispatch(startApplicationEventsStream(id)), + stopStream: id => dispatch(stopApplicationEventsStream(id)), + getApplication: id => dispatch(getApplication(id)), +})) @withSideNavigation(function (props) { const matchedUrl = props.match.url @@ -111,9 +121,16 @@ import Devices from '../devices' export default class Application extends React.Component { componentDidMount () { - const { dispatch, appId } = this.props + const { appId, startStream, getApplication } = this.props + + getApplication(appId) + startStream(appId) + } + + componentWillUnmount () { + const { appId, stopStream } = this.props - dispatch(getApplication(appId)) + stopStream(appId) } render () { @@ -140,6 +157,7 @@ export default class Application extends React.Component { + ) } diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 0165efc8aa..f83535cc75 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -2,7 +2,7 @@ "components.event.index.eventData": "Event Data", "components.events.index.truncated": "Events have been truncated", "components.events.index.showing": "Showing all available events", - "components.events.widget.index.latestEvents": "Latest events", + "components.events.widget.index.latestEvents": "Latest Events", "components.events.widget.index.seeAllActivity": "See all activity", "components.events.widget.index.unknown": "Unknown", "components.footer.index.footer": "You are the network. Let's build this thing together.", @@ -44,6 +44,7 @@ "console.views.application-collaborator-edit.index.deleteSuccess": "Successfully removed collaborator", "console.views.application-collaborator-edit.index.updateSuccess": "Successfully updated collaborator rights", "console.views.application-collaborator-edit.index.modalWarning": "Are you sure you want to remove {collaboratorId} as a collaborator?", + "console.views.application-data.index.appData": "Application Data", "console.views.application-general-settings.index.basics": "Basics", "console.views.application-general-settings.index.deleteApp": "Delete application", "console.views.application-general-settings.index.modalWarning": "Are you sure you want to delete \"{appName}\"? Deleting an application cannot be undone!", diff --git a/pkg/webui/locales/xx.json b/pkg/webui/locales/xx.json index a6ba485e83..aab4940691 100644 --- a/pkg/webui/locales/xx.json +++ b/pkg/webui/locales/xx.json @@ -2,7 +2,7 @@ "components.event.index.eventData": "Xxxxx Xxxx", "components.events.index.truncated": "Xxxxxx xxxx xxxx xxxxxxxxx", "components.events.index.showing": "Xxxxxxx xxx xxxxxxxxx xxxxxx", - "components.events.widget.index.latestEvents": "Xxxxxx xxxxxx", + "components.events.widget.index.latestEvents": "Xxxxxx Xxxxxx", "components.events.widget.index.seeAllActivity": "Xxx xxx xxxxxxxx", "components.events.widget.index.unknown": "Xxxxxxx", "components.footer.index.footer": "Xxx xxx xxx xxxxxxx. Xxx'x xxxxx xxxx xxxxx xxxxxxxx.", @@ -44,6 +44,7 @@ "console.views.application-collaborator-edit.index.deleteSuccess": "Xxxxxxxxxxxx xxxxxxx xxxxxxxxxxxx", "console.views.application-collaborator-edit.index.updateSuccess": "Xxxxxxxxxxxx xxxxxxx xxxxxxxxxxxx xxxxxx", "console.views.application-collaborator-edit.index.modalWarning": "Xxx xxx xxxx xxx xxxx xx xxxxxx {collaboratorId} xx x xxxxxxxxxxxx?", + "console.views.application-data.index.appData": "Xxxxxxxxxxx Xxxx", "console.views.application-general-settings.index.basics": "Xxxxxx", "console.views.application-general-settings.index.deleteApp": "Xxxxxx xxxxxxxxxxx", "console.views.application-general-settings.index.modalWarning": "Xxx xxx xxxx xxx xxxx xx xxxxxx \"{appName}\"? Xxxxxxxx xx xxxxxxxxxxx xxxxxx xx xxxxxx!",