From 406b68a4b41604960ee10adeec5418430ac7bea8 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 16 Aug 2018 12:49:52 -0700 Subject: [PATCH 01/25] Add scaffolding for packages page (#1063) * pakages tab * Packages Folder * Reusable Header Components * package svg * fix for mary ellen's comments * Fix for Stephen's comments * README update 1 * oneProperty declarations * Fix 2 for MaryEllen's comments * 8/8 EOD commit * 8/9 SOD * configModels * isaac-d-t * 8 13 localhost * remove package title and header * complete reducer * crazy translation error * scafolding bug fixes * Revert "8 13 localhost" This reverts commit 9de75d0e60ada92fd828d98cac41361c73c45890. * readme merge * fix for mary ellen's cooments 1 * remove css from svg * removing warnings * copy right and spacing * work in progress --- public/locales/en/translations.json | 24 +++- src/assets/icons/packages.svg | 4 + src/components/app/app.js | 25 ++-- src/components/pages/index.js | 1 + .../pages/packages/flyouts/index.js | 3 + .../packages/flyouts/newPackage/index.js | 3 + .../newPackage/newPackage.container.js | 13 ++ .../packages/flyouts/newPackage/newPackage.js | 13 ++ .../flyouts/newPackage/newPackage.scss | 3 + .../pages/packages/packages.container.js | 30 +++++ src/components/pages/packages/packages.js | 82 ++++++++++++ src/components/pages/packages/packages.scss | 3 + .../pages/packages/packages.test.js | 28 ++++ .../pages/packages/packagesGrid/index.js | 4 + .../packages/packagesGrid/packagesGrid.js | 12 ++ .../packages/packagesGrid/packagesGrid.scss | 3 + .../packagesGrid/packagesGridConfig.js | 15 +++ src/components/shared/pageStats/README.md | 2 +- src/services/configService.js | 22 +++- src/services/models/configModels.js | 22 ++++ src/store/reducers/packagesReducer.js | 123 ++++++++++++++++++ src/store/rootEpic.js | 2 + src/store/rootReducer.js | 2 + src/utilities/svgs.js | 2 + 24 files changed, 423 insertions(+), 18 deletions(-) create mode 100644 src/assets/icons/packages.svg create mode 100644 src/components/pages/packages/flyouts/index.js create mode 100644 src/components/pages/packages/flyouts/newPackage/index.js create mode 100644 src/components/pages/packages/flyouts/newPackage/newPackage.container.js create mode 100644 src/components/pages/packages/flyouts/newPackage/newPackage.js create mode 100644 src/components/pages/packages/flyouts/newPackage/newPackage.scss create mode 100644 src/components/pages/packages/packages.container.js create mode 100644 src/components/pages/packages/packages.js create mode 100644 src/components/pages/packages/packages.scss create mode 100644 src/components/pages/packages/packages.test.js create mode 100644 src/components/pages/packages/packagesGrid/index.js create mode 100644 src/components/pages/packages/packagesGrid/packagesGrid.js create mode 100644 src/components/pages/packages/packagesGrid/packagesGrid.scss create mode 100644 src/components/pages/packages/packagesGrid/packagesGridConfig.js create mode 100644 src/store/reducers/packagesReducer.js diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 6c0611256..176c36fad 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -17,6 +17,7 @@ "devices": "Devices", "rules": "Rules", "maintenance": "Maintenance", + "packages": "Packages", "example": "Page Example", "gridExample": "Grid Example", "flyoutExample": "Flyout Example" @@ -354,16 +355,17 @@ }, "SIMManagement": { "title": "SIM Management", - "provider" : "Provider", + "provider": "Provider", "summaryHeader": "Summary and instructions", "here": "here", "select": "Select...", - "header":{ - "telefonica": "As a Telefónica IoT customer you have the advantage of adding this feature to enrich automatically your Azure Remote Monitoring Solution with the connectivity data available in Telefónica IoT Connectivity Platform (Network Information, Data Consumption & Network Based Location)." }, + "header": { + "telefonica": "As a Telefónica IoT customer you have the advantage of adding this feature to enrich automatically your Azure Remote Monitoring Solution with the connectivity data available in Telefónica IoT Connectivity Platform (Network Information, Data Consumption & Network Based Location)." + }, "description": { "telefonica": "This feature is in Preview. In order to sync your connectivity data into Azure Remote Monitoring Solution, please fill a request <1><0>url, select the option “Azure Remote Monitoring” and include your contact data and we will automatically activate your account. \n\nIf you are not a Telefónica client yet and you want to enjoy this or other IoT Connectivity Cloud Ready services, you can contact us <1><0>url and select the option “Connectivity”; we will be glad to help you." }, - "operator":{ + "operator": { "telefonica": "Telefónica" } } @@ -540,6 +542,18 @@ "P7D": "Last week", "P1M": "Last month" }, + "packages": { + "searchPlaceholder": "Search packages...", + "noneFound": "No packages found.", + "title": "Packages", + "total": "total packages", + "grid": {}, + "flyouts": { + "new": { + "contextMenuName": "New Package" + } + } + }, "examples": { "pagePlaceholder": "This is a new page.", "grid": { @@ -553,7 +567,7 @@ "pageBody": "Click the context button above to open a flyout.", "open": "Open Flyout", "flyouts": { - "example" : { + "example": { "header": "Example Flyout", "description": "This example flyout contains a simple form with action buttons.", "insertFormHere": "Insert form controls here. See 'src/components/shared/forms' for form specific shared controls you might leverage.", diff --git a/src/assets/icons/packages.svg b/src/assets/icons/packages.svg new file mode 100644 index 000000000..96b32c6c1 --- /dev/null +++ b/src/assets/icons/packages.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/app/app.js b/src/components/app/app.js index 01d5331d0..c39dffb3b 100644 --- a/src/components/app/app.js +++ b/src/components/app/app.js @@ -12,11 +12,12 @@ import NavigationContainer from './navigation/navigationContainer'; import Main from './main/main'; // Page Components -import { +import { DashboardContainer as DashboardPage, DevicesContainer as DevicesPage, RulesContainer as RulesPage, MaintenanceContainer as MaintenancePage, + PackagesContainer as PackagesPage, ExampleContainer as ExamplePage, FlyoutExampleContainer as FlyoutExamplePage, GridExampleContainer as GridExamplePage, @@ -28,14 +29,15 @@ import { svgs } from 'utilities'; import './app.css'; /** The navigation tab configurations */ -const dashboardTab = { to: '/dashboard', svg: svgs.tabs.dashboard, labelId: 'tabs.dashboard' }; -const devicesTab = { to: '/devices', svg: svgs.tabs.devices, labelId: 'tabs.devices' }; -const rulesTab = { to: '/rules', svg: svgs.tabs.rules, labelId: 'tabs.rules' }; -const maintenanceTab = { to: '/maintenance', svg: svgs.tabs.maintenance, labelId: 'tabs.maintenance' }; -const exampleTab = { to: '/example', svg: svgs.tabs.example, labelId: 'tabs.example' }; -const flyoutExampleTab = { to: '/flyoutexample', svg: svgs.tabs.example, labelId: 'tabs.flyoutExample' }; -const gridExampleTab = { to: '/gridexample', svg: svgs.tabs.example, labelId: 'tabs.gridExample' }; -const tabConfigs = [ dashboardTab, devicesTab, rulesTab, maintenanceTab ]; +const dashboardTab = { to: '/dashboard', svg: svgs.tabs.dashboard, labelId: 'tabs.dashboard' }; +const devicesTab = { to: '/devices', svg: svgs.tabs.devices, labelId: 'tabs.devices' }; +const rulesTab = { to: '/rules', svg: svgs.tabs.rules, labelId: 'tabs.rules' }; +const maintenanceTab = { to: '/maintenance', svg: svgs.tabs.maintenance, labelId: 'tabs.maintenance' }; +const packagesTab = { to: '/packages', svg: svgs.tabs.packages, labelId: 'tabs.packages' }; +const exampleTab = { to: '/example', svg: svgs.tabs.example, labelId: 'tabs.example' }; +const flyoutExampleTab = { to: '/flyoutexample', svg: svgs.tabs.example, labelId: 'tabs.flyoutExample' }; +const gridExampleTab = { to: '/gridexample', svg: svgs.tabs.example, labelId: 'tabs.gridExample' }; +const tabConfigs = [dashboardTab, devicesTab, rulesTab, maintenanceTab, packagesTab]; /** Only show example pages and components when configured to do so */ if (Config.showWalkthroughExamples) { @@ -76,13 +78,14 @@ class App extends Component { + - { this.props.deviceGroupFlyoutIsOpen && } - { this.state.openFlyout === 'settings' && } + {this.props.deviceGroupFlyoutIsOpen && } + {this.state.openFlyout === 'settings' && } diff --git a/src/components/pages/index.js b/src/components/pages/index.js index ec71a3a2e..121e50c59 100644 --- a/src/components/pages/index.js +++ b/src/components/pages/index.js @@ -11,3 +11,4 @@ export * from './rules/rules.container'; export * from './maintenance/maintenance.container'; export * from './pageNotFound/pageNotFound'; export * from './pageNotFound/pageNotFound.container'; +export * from './packages/packages.container'; diff --git a/src/components/pages/packages/flyouts/index.js b/src/components/pages/packages/flyouts/index.js new file mode 100644 index 000000000..485cef754 --- /dev/null +++ b/src/components/pages/packages/flyouts/index.js @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './newPackage'; diff --git a/src/components/pages/packages/flyouts/newPackage/index.js b/src/components/pages/packages/flyouts/newPackage/index.js new file mode 100644 index 000000000..485cef754 --- /dev/null +++ b/src/components/pages/packages/flyouts/newPackage/index.js @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './newPackage'; diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.container.js b/src/components/pages/packages/flyouts/newPackage/newPackage.container.js new file mode 100644 index 000000000..b62f8092e --- /dev/null +++ b/src/components/pages/packages/flyouts/newPackage/newPackage.container.js @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { NewPackage } from './newPackage'; +import { epics as packagesEpics } from 'store/reducers/packagesReducer'; + +// Wrap the dispatch methods +const mapDispatchToProps = dispatch => ({ + createPackage: (packageObj) => dispatch(packagesEpics.actions.createPackage(packageObj)) +}); + +export const NewPackageContainer = translate()(connect(null, mapDispatchToProps)(NewPackage)); diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.js b/src/components/pages/packages/flyouts/newPackage/newPackage.js new file mode 100644 index 000000000..37e3ace42 --- /dev/null +++ b/src/components/pages/packages/flyouts/newPackage/newPackage.js @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { LinkedComponent } from 'utilities'; + +import './newPackage.css'; + +export class NewPackage extends LinkedComponent { + // TODO + + render() { + return ([]); + } +} diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.scss b/src/components/pages/packages/flyouts/newPackage/newPackage.scss new file mode 100644 index 000000000..02febbecd --- /dev/null +++ b/src/components/pages/packages/flyouts/newPackage/newPackage.scss @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +// TODO - Work in progress diff --git a/src/components/pages/packages/packages.container.js b/src/components/pages/packages/packages.container.js new file mode 100644 index 000000000..57d82e0d3 --- /dev/null +++ b/src/components/pages/packages/packages.container.js @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { Packages } from './packages'; +import { + epics as packagesEpics, + getPackages, + getEntities, + getPackagesError, + getPackagesLastUpdated, + getPackagesPendingStatus +} from 'store/reducers/packagesReducer'; + +// Pass the packages status +const mapStateToProps = state => ({ + entities: getEntities(state), + packages: getPackages(state), + error: getPackagesError(state), + isPending: getPackagesPendingStatus(state), + lastUpdated: getPackagesLastUpdated(state) +}); + +// Wrap the dispatch method +const mapDispatchToProps = dispatch => ({ + fetchPackages: () => dispatch(packagesEpics.actions.fetchPackages()), + deletePackages: (packageIdArr) => dispatch(packagesEpics.actions.deletePackages(packageIdArr)) +}); + +export const PackagesContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Packages)); diff --git a/src/components/pages/packages/packages.js b/src/components/pages/packages/packages.js new file mode 100644 index 000000000..aef192459 --- /dev/null +++ b/src/components/pages/packages/packages.js @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; +import { permissions } from 'services/models'; +import { PackagesGrid } from './packagesGrid'; +import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn'; +import { + AjaxError, + Btn, + ContextMenu, + PageContent, + Protected, + RefreshBar + } from 'components/shared'; + import { NewPackage } from './flyouts'; + import { svgs } from 'utilities'; + +import './packages.css'; + +const closedFlyoutState = { openFlyoutName: undefined }; + +export class Packages extends Component { + constructor(props) { + super(props); + this.state = { + ...closedFlyoutState, + contextBtns: null + }; + + if (!this.props.lastUpdated && !this.props.error) { + this.props.fetchPackages(); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.isPending && nextProps.isPending !== this.props.isPending) { + // If the grid data refreshes, hide the flyout and deselect soft selections + this.setState(closedFlyoutState); + } + } + + closeFlyout = () => this.setState(closedFlyoutState); + + openNewPackageFlyout = () => this.setState({ + openFlyoutName: 'newPackage', + selectedPackageId: '' + }); + + onGridReady = gridReadyEvent => this.packageGridApi = gridReadyEvent.api; + + searchOnChange = ({ target: { value } }) => { + if (this.packageGridApi) this.packageGridApi.setQuickFilter(value); + }; + + onContextMenuChange = contextBtns => this.setState({ contextBtns }); + + render() { + const { t, packages, error, isPending, fetchPackages, lastUpdated } = this.props; + const gridProps = { + onGridReady: this.onGridReady, + rowData: isPending ? undefined : packages || [], + onContextMenuChange: this.onContextMenuChange, + t: this.props.t + }; + + return [ + + {this.state.contextBtns} + + {t('packages.flyouts.new.contextMenuName')} + + + , + + + {!!error && } + {!error && } + {this.state.openFlyoutName === 'newPackage' && } + + ]; + } +} diff --git a/src/components/pages/packages/packages.scss b/src/components/pages/packages/packages.scss new file mode 100644 index 000000000..cca58d691 --- /dev/null +++ b/src/components/pages/packages/packages.scss @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +// TODO diff --git a/src/components/pages/packages/packages.test.js b/src/components/pages/packages/packages.test.js new file mode 100644 index 000000000..c55a182fe --- /dev/null +++ b/src/components/pages/packages/packages.test.js @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + + +import React from 'react'; +import { shallow } from 'enzyme'; +import 'polyfills'; + +import { Packages } from './packages'; + +describe('Packages Component', () => { + it('Renders without crashing', () => { + + const fakeProps = { + packages: {}, + entities: {}, + error: undefined, + isPending: false, + lastUpdated: undefined, + fetchPackages: () => {}, + deletePackages: (p) => {}, + t: () => {}, + }; + + const wrapper = shallow( + + ); + }); +}); diff --git a/src/components/pages/packages/packagesGrid/index.js b/src/components/pages/packages/packagesGrid/index.js new file mode 100644 index 000000000..e1153530a --- /dev/null +++ b/src/components/pages/packages/packagesGrid/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './packagesGridConfig'; +export * from './packagesGrid'; diff --git a/src/components/pages/packages/packagesGrid/packagesGrid.js b/src/components/pages/packages/packagesGrid/packagesGrid.js new file mode 100644 index 000000000..b8a8fe345 --- /dev/null +++ b/src/components/pages/packages/packagesGrid/packagesGrid.js @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. +import { Component } from 'react'; + +import './packagesGrid.css'; + +export class PackagesGrid extends Component { + // TODO + + render (){ + return []; + } +} diff --git a/src/components/pages/packages/packagesGrid/packagesGrid.scss b/src/components/pages/packages/packagesGrid/packagesGrid.scss new file mode 100644 index 000000000..cca58d691 --- /dev/null +++ b/src/components/pages/packages/packagesGrid/packagesGrid.scss @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +// TODO diff --git a/src/components/pages/packages/packagesGrid/packagesGridConfig.js b/src/components/pages/packages/packagesGrid/packagesGridConfig.js new file mode 100644 index 000000000..c1a74a053 --- /dev/null +++ b/src/components/pages/packages/packagesGrid/packagesGridConfig.js @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +import Config from 'app.config'; + +export const packagesColumnDefs = { + // TODO +}; + +export const defaultPackagesGridProps = { + enableColResize: true, + multiSelect: true, + pagination: true, + paginationPageSize: Config.paginationPageSize, + rowSelection: 'multiple' +}; diff --git a/src/components/shared/pageStats/README.md b/src/components/shared/pageStats/README.md index 26c35b712..d43de82b2 100644 --- a/src/components/shared/pageStats/README.md +++ b/src/components/shared/pageStats/README.md @@ -13,7 +13,7 @@ A presentational component containing one or many StatPropertys. By default the ### StatProperty:  -A presentational component containing number value, label, and an optional svg and svgClassname. The number value can be of three different sizes- large, medium or small, based on `size` parameter. By default the 'size' will be assigned 'normal', which will inherit it's font size from parent css class. +A presentational component containing number value, label, and an optional svg and svgClassname. The number value can be of three different sizes- large, medium or small, based on `size` parameter. By default the 'size' will be assigned 'small'. ## Examples:  diff --git a/src/services/configService.js b/src/services/configService.js index bc5d12976..579ccd744 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -6,7 +6,9 @@ import { prepareLogoResponse, toDeviceGroupModel, toDeviceGroupsModel, - toSolutionSettingThemeModel + toSolutionSettingThemeModel, + toNewPackageRequestModel, + toPackagesModel } from './models'; import { Observable } from '../../node_modules/rxjs'; @@ -85,4 +87,22 @@ export class ConfigService { return HttpClient.put(`${ENDPOINT}solution-settings/theme`, model) .map(toSolutionSettingThemeModel); } + + /** Creates a new package */ + static createPackage(payload) { + return HttpClient.post(`${ENDPOINT}packages`, payload) + .map(toNewPackageRequestModel); + } + + /** Returns all the account's packages */ + static getPackages() { + return HttpClient.get(`${ENDPOINT}packages`) + .map(toPackagesModel); + } + + /** Delete a package */ + static deletePackage(id) { + return HttpClient.delete(`${ENDPOINT}packages/${id}`) + .map(_ => id); + } } diff --git a/src/services/models/configModels.js b/src/services/models/configModels.js index bc4e8c02f..d0ffdbaee 100644 --- a/src/services/models/configModels.js +++ b/src/services/models/configModels.js @@ -53,3 +53,25 @@ export const toSolutionSettingThemeModel = (response = {}) => camelCaseReshape(r 'diagnosticsOptIn': 'diagnosticsOptIn', 'azureMapsKey': 'azureMapsKey' }); + +export const toNewPackageRequestModel = ({ + type, + packageObj +}) => { + return { + Type: type, + Package: packageObj + }; +} + +export const toPackagesModel = (response = {}) => getItems(response) + .map(toPackageModel); + +export const toPackageModel = (response = {}) => { + return camelCaseReshape(response, { + 'id': 'Id', + 'type': 'Type', + 'name': 'Name', + 'dateCreated': 'DateCreated' + }); +}; diff --git a/src/store/reducers/packagesReducer.js b/src/store/reducers/packagesReducer.js new file mode 100644 index 000000000..75d4a1e7b --- /dev/null +++ b/src/store/reducers/packagesReducer.js @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft. All rights reserved. + +import 'rxjs'; +import { Observable } from 'rxjs'; +import moment from 'moment'; +import { schema, normalize } from 'normalizr'; +import update from 'immutability-helper'; +import { createSelector } from 'reselect'; +import { ConfigService } from 'services'; +import { + createReducerScenario, + createEpicScenario, + errorPendingInitialState, + pendingReducer, + errorReducer, + setPending, + getPending, + getError, + toActionCreator +} from 'store/utilities'; + +// ========================= Epics - START +const handleError = fromAction => error => + Observable.of(redux.actions.registerError(fromAction.type, { error, fromAction })); + +export const epics = createEpicScenario({ + /** Loads Packages*/ + fetchPackages: { + type: 'PACKAGES_FETCH', + epic: fromAction => + ConfigService.getPackages() + .map(toActionCreator(redux.actions.updatePackages, fromAction)) + .catch(handleError(fromAction)) + }, + /** Create a new package */ + createPackage: { + type: 'PACKAGES_CREATE', + epic: fromAction => + ConfigService.createPackage(fromAction.payload) + .map(toActionCreator(redux.actions.insertPackage, fromAction)) + .catch(handleError(fromAction)) + }, + /** Delete packages */ + deletePackages: { + type: 'PACKAGES_DELETE', + epic: fromAction => + ConfigService.deletePackage(fromAction.payload) + .map(toActionCreator(redux.actions.deletePackage, fromAction)) + .catch(handleError(fromAction)) + } +}); +// ========================= Epics - END + +// ========================= Schemas - START +const packageSchema = new schema.Entity('packages'); +const packageListSchema = new schema.Array(packageSchema); +// ========================= Schemas - END + +// ========================= Reducers - START +const initialState = { ...errorPendingInitialState, entities: {} }; + +const insertPackageReducer = (state, { payload }) => { + const { entities: { packages }, result } = normalize(payload, packageListSchema); + return update(state, { + entities: { $merge: packages }, + items: { $splice: [[state.items.length, 0, result]] } + }); +}; + +const deletePackagesReducer = (state, { payload }) => { + const spliceArr = payload.reduce((idxAcc, payloadItem) => { + const idx = state.items.indexOf(payloadItem); + if (idx !== -1) { + idxAcc.push([idx, 1]); + } + return idxAcc; + }, []); + return update(state, { + entities: { $unset: payload }, + items: { $splice: spliceArr } + }); +}; + +const updatePackagesReducer = (state, { payload, fromAction }) => { + const { entities: { packages }, result } = normalize(payload, packageListSchema); + return update(state, { + entities: { $set: packages }, + items: { $set: result }, + lastUpdated: { $set: moment() }, + ...setPending(fromAction.type, false) + }); +}; + +/* Action types that cause a pending flag */ +const fetchableTypes = [ + epics.actionTypes.fetchPackages +]; + +export const redux = createReducerScenario({ + insertPackage: { type: 'PACKAGE_INSERT', reducer: insertPackageReducer }, + deletePackages: { type: 'PACKAGES_DELETE', reducer: deletePackagesReducer }, + updatePackages: { type: 'PACKAGES_UPDATE', reducer: updatePackagesReducer }, + registerError: { type: 'PACKAGES_REDUCER_ERROR', reducer: errorReducer }, + isFetching: { multiType: fetchableTypes, reducer: pendingReducer } +}); + +export const reducer = { packages: redux.getReducer(initialState) }; +// ========================= Reducers - END + +// ========================= Selectors - START +export const getPackagesReducer = state => state.packages; +export const getEntities = state => getPackagesReducer(state).entities || {}; +export const getItems = state => getPackagesReducer(state).items || []; +export const getPackagesLastUpdated = state => getPackagesReducer(state).lastUpdated; +export const getPackagesError = state => + getError(getPackagesReducer(state), epics.actionTypes.fetchPackages); +export const getPackagesPendingStatus = state => + getPending(getPackagesReducer(state), epics.actionTypes.fetchPackages); +export const getPackages = createSelector( + getEntities, getItems, + (entities, items) => items.map(id => entities[id]) +); +// ========================= Selectors - END diff --git a/src/store/rootEpic.js b/src/store/rootEpic.js index af21d05ee..12e553d7f 100644 --- a/src/store/rootEpic.js +++ b/src/store/rootEpic.js @@ -7,6 +7,7 @@ import { epics as exampleEpics } from './reducers/_exampleReducer'; import { epics as appEpics } from './reducers/appReducer'; import { epics as devicesEpics } from './reducers/devicesReducer'; import { epics as rulesEpics } from './reducers/rulesReducer'; +import { epics as packagesEpics } from './reducers/packagesReducer'; import { epics as simulationEpics } from './reducers/deviceSimulationReducer'; // Extract the epic function from each property object @@ -14,6 +15,7 @@ const epics = [ ...exampleEpics.getEpics(), ...appEpics.getEpics(), ...devicesEpics.getEpics(), + ...packagesEpics.getEpics(), ...rulesEpics.getEpics(), ...simulationEpics.getEpics() ]; diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js index b383bce0a..746e5c0e8 100644 --- a/src/store/rootReducer.js +++ b/src/store/rootReducer.js @@ -8,11 +8,13 @@ import { reducer as appReducer } from './reducers/appReducer'; import { reducer as simulationReducer } from './reducers/deviceSimulationReducer'; import { reducer as devicesReducer } from './reducers/devicesReducer'; import { reducer as rulesReducer } from './reducers/rulesReducer'; +import { reducer as packagesReducer } from './reducers/packagesReducer'; const rootReducer = combineReducers({ ...exampleReducer, ...appReducer, ...devicesReducer, + ...packagesReducer, ...rulesReducer, ...simulationReducer }); diff --git a/src/utilities/svgs.js b/src/utilities/svgs.js index c690a62f3..b1b62c28e 100644 --- a/src/utilities/svgs.js +++ b/src/utilities/svgs.js @@ -31,6 +31,7 @@ import InfoIconPath from 'assets/icons/info.svg'; import LinkToPath from 'assets/icons/linkTo.svg'; import LoadingToggleIconPath from 'assets/icons/loadingToggle.svg'; import ManageFiltersIconPath from 'assets/icons/manageFilters.svg'; +import TabPackagesIconPath from 'assets/icons/packages.svg'; import PhysicalDeviceIconPath from 'assets/icons/physicalDevice.svg'; import PlusIconPath from 'assets/icons/plus.svg'; import QuestionMarkIconPath from 'assets/icons/questionMark.svg'; @@ -59,6 +60,7 @@ export const svgs = { dashboard: TabDashboardIconPath, devices: TabDevicesIconPath, maintenance: TabMaintenanceIconPath, + packages: TabPackagesIconPath, rules: TabRulesIconPath, example: InfoBubbleIconPath }, From 04b245065b6140ed9ced7820e3bbbea31983d012 Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 17 Aug 2018 16:48:27 -0700 Subject: [PATCH 02/25] Display content in packages grid (#1068) * pakages tab * Packages Folder * Reusable Header Components * package svg * fix for mary ellen's comments * Fix for Stephen's comments * README update 1 * oneProperty declarations * Fix 2 for MaryEllen's comments * 8/8 EOD commit * 8/9 SOD * configModels * isaac-d-t * 8 13 localhost * remove package title and header * complete reducer * crazy translation error * scafolding bug fixes * Revert "8 13 localhost" This reverts commit 9de75d0e60ada92fd828d98cac41361c73c45890. * readme merge * Revert "Revert "8 13 localhost"" This reverts commit 2b42e8c8ca551a2a614e29d9d0fc2189f18bb147. * fix for mary ellen's cooments 1 * remove css from svg * removing warnings * packagesColumnDefs * copy right and spacing * work in progress * Display data in grid * reset app.config * fix for mary ellen's comments --- public/locales/en/translations.json | 10 ++- .../pages/packages/packages.container.js | 2 - src/components/pages/packages/packages.js | 34 ++++--- src/components/pages/packages/packages.scss | 14 ++- .../packages/packagesGrid/packagesGrid.js | 88 ++++++++++++++++++- .../packagesGrid/packagesGridConfig.js | 20 ++++- .../shared/pageTitle/pageTitle.scss | 8 +- src/services/models/authModels.js | 5 +- src/services/models/configModels.js | 8 +- 9 files changed, 155 insertions(+), 34 deletions(-) diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 176c36fad..711297647 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -547,10 +547,16 @@ "noneFound": "No packages found.", "title": "Packages", "total": "total packages", - "grid": {}, + "new": "New Package", + "delete": "Delete", + "grid": { + "name": "Name", + "type": "Type", + "dateCreated": "Date Created" + }, "flyouts": { "new": { - "contextMenuName": "New Package" + "title": "New Package" } } }, diff --git a/src/components/pages/packages/packages.container.js b/src/components/pages/packages/packages.container.js index 57d82e0d3..f6c59bb8a 100644 --- a/src/components/pages/packages/packages.container.js +++ b/src/components/pages/packages/packages.container.js @@ -6,7 +6,6 @@ import { Packages } from './packages'; import { epics as packagesEpics, getPackages, - getEntities, getPackagesError, getPackagesLastUpdated, getPackagesPendingStatus @@ -14,7 +13,6 @@ import { // Pass the packages status const mapStateToProps = state => ({ - entities: getEntities(state), packages: getPackages(state), error: getPackagesError(state), isPending: getPackagesPendingStatus(state), diff --git a/src/components/pages/packages/packages.js b/src/components/pages/packages/packages.js index aef192459..e72837b76 100644 --- a/src/components/pages/packages/packages.js +++ b/src/components/pages/packages/packages.js @@ -3,17 +3,17 @@ import React, { Component } from 'react'; import { permissions } from 'services/models'; import { PackagesGrid } from './packagesGrid'; -import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn'; import { AjaxError, Btn, ContextMenu, PageContent, Protected, - RefreshBar - } from 'components/shared'; - import { NewPackage } from './flyouts'; - import { svgs } from 'utilities'; + RefreshBar, + PageTitle +} from 'components/shared'; +import { NewPackage } from './flyouts'; +import { svgs } from 'utilities'; import './packages.css'; @@ -34,26 +34,24 @@ export class Packages extends Component { componentWillReceiveProps(nextProps) { if (nextProps.isPending && nextProps.isPending !== this.props.isPending) { - // If the grid data refreshes, hide the flyout and deselect soft selections + // If the grid data refreshes, hide the flyout this.setState(closedFlyoutState); } } closeFlyout = () => this.setState(closedFlyoutState); + onContextMenuChange = contextBtns => this.setState({ + contextBtns, + openFlyoutName: undefined + }); + openNewPackageFlyout = () => this.setState({ - openFlyoutName: 'newPackage', - selectedPackageId: '' + openFlyoutName: 'new-Package' }); onGridReady = gridReadyEvent => this.packageGridApi = gridReadyEvent.api; - searchOnChange = ({ target: { value } }) => { - if (this.packageGridApi) this.packageGridApi.setQuickFilter(value); - }; - - onContextMenuChange = contextBtns => this.setState({ contextBtns }); - render() { const { t, packages, error, isPending, fetchPackages, lastUpdated } = this.props; const gridProps = { @@ -65,14 +63,14 @@ export class Packages extends Component { return [ + {this.state.contextBtns} - - {t('packages.flyouts.new.contextMenuName')} + + {t('packages.new')} - , - + {!!error && } {!error && } {this.state.openFlyoutName === 'newPackage' && } diff --git a/src/components/pages/packages/packages.scss b/src/components/pages/packages/packages.scss index cca58d691..d01067d73 100644 --- a/src/components/pages/packages/packages.scss +++ b/src/components/pages/packages/packages.scss @@ -1,3 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. -// TODO +@import 'src/styles/variables'; +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.package-container { + display: flex; + flex-flow: column nowrap; + padding: $baseContentPadding; + + .package-title { + @include rem-fallback(padding-bottom, 30px); + } +} diff --git a/src/components/pages/packages/packagesGrid/packagesGrid.js b/src/components/pages/packages/packagesGrid/packagesGrid.js index b8a8fe345..c7d1b5dd6 100644 --- a/src/components/pages/packages/packagesGrid/packagesGrid.js +++ b/src/components/pages/packages/packagesGrid/packagesGrid.js @@ -1,12 +1,92 @@ // Copyright (c) Microsoft. All rights reserved. -import { Component } from 'react'; +import React, { Component } from 'react'; +import { permissions } from 'services/models'; +import { packagesColumnDefs, defaultPackagesGridProps } from './packagesGridConfig'; +import { Btn, PcsGrid, Protected } from 'components/shared'; +import { isFunc, translateColumnDefs, svgs } from 'utilities'; +import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig'; import './packagesGrid.css'; +const closedFlyoutState = { + openFlyoutName: undefined +}; + export class PackagesGrid extends Component { - // TODO + constructor(props) { + super(props); + + // Set the initial state + this.state = { + ...closedFlyoutState + }; + + this.columnDefs = [ + checkboxColumn, + packagesColumnDefs.name, + packagesColumnDefs.type, + packagesColumnDefs.dateCreated + ]; + + this.contextBtns = [ + + {props.t('packages.delete')} + + ]; + } + + /** + * Get the grid api options + * + * @param {Object} gridReadyEvent An object containing access to the grid APIs + */ + onGridReady = gridReadyEvent => { + this.packagesGridApi = gridReadyEvent.api; + // Call the onReady props if it exists + if (isFunc(this.props.onGridReady)) { + this.props.onGridReady(gridReadyEvent); + } + }; + + /** + * Handles context filter changes and calls any hard select props method + * + * @param {Array} selectedPackages A list of currently selected packages + */ + onHardSelectChange = (selectedPackages) => { + const { onContextMenuChange, onHardSelectChange } = this.props; + if (isFunc(onContextMenuChange)) { + onContextMenuChange(selectedPackages.length > 0 ? this.contextBtns : null); + } + if (isFunc(onHardSelectChange)) { + onHardSelectChange(selectedPackages); + } + } + + closeFlyout = () => this.setState(closedFlyoutState); + + openFlyout = (flyoutName) => () => this.setState({ + openFlyoutName: flyoutName + }); + + render() { + const gridProps = { + /* Grid Properties */ + ...defaultPackagesGridProps, + columnDefs: translateColumnDefs(this.props.t, this.columnDefs), + sizeColumnsToFit: true, + deltaRowDataMode: true, + ...this.props, // Allow default property overrides + onGridReady: event => this.onGridReady(event), // Wrap in a function to avoid closure issues + getRowNodeId: ({ id }) => id, + enableSorting: true, + unSortIcon: true, + onHardSelectChange: this.onHardSelectChange, + context: { + t: this.props.t + } + }; - render (){ - return []; + return (); } } diff --git a/src/components/pages/packages/packagesGrid/packagesGridConfig.js b/src/components/pages/packages/packagesGrid/packagesGridConfig.js index c1a74a053..5fa62427f 100644 --- a/src/components/pages/packages/packagesGrid/packagesGridConfig.js +++ b/src/components/pages/packages/packagesGrid/packagesGridConfig.js @@ -1,9 +1,27 @@ // Copyright (c) Microsoft. All rights reserved. import Config from 'app.config'; +import { TimeRenderer } from 'components/shared/cellRenderers'; +import { gridValueFormatters } from 'components/shared/pcsGrid/pcsGridConfig'; + +const { checkForEmpty } = gridValueFormatters; export const packagesColumnDefs = { - // TODO + name: { + headerName: 'packages.grid.name', + field: 'name', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + type: { + headerName: 'packages.grid.type', + field: 'type', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + dateCreated: { + headerName: 'packages.grid.dateCreated', + field: 'dateCreated', + cellRendererFramework: TimeRenderer + } }; export const defaultPackagesGridProps = { diff --git a/src/components/shared/pageTitle/pageTitle.scss b/src/components/shared/pageTitle/pageTitle.scss index faf4c595e..5e7d30632 100644 --- a/src/components/shared/pageTitle/pageTitle.scss +++ b/src/components/shared/pageTitle/pageTitle.scss @@ -1,10 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. +@import 'src/styles/variables'; @import 'src/styles/mixins'; +@import 'src/styles/themes'; .page-title { font-weight: 700; margin: 0; - margin-right: 10px; + @include rem-fallback(margin-right, 10px); @include rem-font-size(48px); + + @include themify($themes) { + color: themed('colorHeaderText'); + } } diff --git a/src/services/models/authModels.js b/src/services/models/authModels.js index ec39d9433..4ba62396b 100644 --- a/src/services/models/authModels.js +++ b/src/services/models/authModels.js @@ -21,7 +21,10 @@ export const permissions = { createJobs: 'CreateJobs', - updateSIMManagement: 'UpdateSIMManagement' + updateSIMManagement: 'UpdateSIMManagement', + + deletePackages: 'DeletePackages', + addPackages: 'AddPackages' }; export const toUserModel = (user = {}) => camelCaseReshape(user, { diff --git a/src/services/models/configModels.js b/src/services/models/configModels.js index d0ffdbaee..236ff8c77 100644 --- a/src/services/models/configModels.js +++ b/src/services/models/configModels.js @@ -69,9 +69,9 @@ export const toPackagesModel = (response = {}) => getItems(response) export const toPackageModel = (response = {}) => { return camelCaseReshape(response, { - 'id': 'Id', - 'type': 'Type', - 'name': 'Name', - 'dateCreated': 'DateCreated' + 'id': 'id', + 'type': 'type', + 'name': 'name', + 'dateCreated': 'dateCreated' }); }; From 0addf66fff857cfd4dab02ca774a813f43448b8f Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 23 Aug 2018 16:23:44 -0700 Subject: [PATCH 03/25] Merge Master into Edge Feature Branch (#1073) * Dev Walkthru: add a new Panel to the Dashboard (#1062) * Dev Walkthru: add a new Panel to the Dashboard * small tweaks, review feedback * fix bad code end marker * Diagnostics bugFix (#1065) * flatMap * Dummy comment to retrigger build * Add Rule Diagnostics (#1064) Add diagnostics logging for rule create/update events. Added the following metrics: Rule_NewClick Rule_EditClick Rule_DeviceGroupClick Rule_CalculationClick Rule_FieldClick Rule_OperatorClick Rule_AddConditionClick Rule_SeverityLevelClick Rule_StatusToggle Rule_ApplyClick Rule_CancelClick Rule_TopXCloseClick Also includes new "sessionid" sections of diagnostics call, which logs the time in ms since Jan 1, 1970 when the page was loaded (amplitude expects session id in this format). This fields will be added by diagnostics to enable logging of session id to amplitude--until those changes go in it will be ignored by the backend. --- docs/walkthrough/README.md | 1 + docs/walkthrough/addNewDashboardPanel.md | 75 +++++++++++++++++++ docs/walkthrough/addNewGrid.md | 2 +- docs/walkthrough/addNewService.md | 2 +- public/locales/en/translations.json | 4 + src/components/app/app.js | 23 +++++- .../pages/dashboard/dashboard.container.js | 3 +- src/components/pages/dashboard/dashboard.js | 8 ++ .../pages/dashboard/dashboard.test.js | 3 +- .../panels/_examplePanel/examplePanel.js | 35 +++++++++ .../panels/_examplePanel/examplePanel.scss | 14 ++++ .../dashboard/panels/_examplePanel/index.js | 3 + .../pages/dashboard/panels/index.js | 1 + .../pages/devices/devices.container.js | 9 ++- src/components/pages/devices/devices.js | 2 + src/components/pages/devices/devices.test.js | 1 + .../maintenance/maintenance.container.js | 5 +- .../pages/maintenance/maintenance.js | 4 +- .../pages/maintenance/maintenance.test.js | 3 +- .../maintenance/ruleDetails/ruleDetails.js | 3 +- .../pages/rules/flyouts/editRuleFlyout.js | 58 ++++++++------ .../pages/rules/flyouts/newRuleFlyout.js | 42 +++++++---- .../pages/rules/flyouts/ruleDetailsFlyout.js | 17 ++++- .../rules/flyouts/ruleEditor/ruleEditor.js | 65 ++++++++++++---- src/components/pages/rules/rules.container.js | 10 ++- src/components/pages/rules/rules.js | 24 ++++-- src/components/pages/rules/rules.test.js | 3 +- .../pages/rules/rulesGrid/rulesGrid.js | 11 ++- src/services/models/diagnosticsModels.js | 3 +- src/services/models/logEventModels.js | 27 +++++-- src/store/reducers/appReducer.js | 21 +++++- 31 files changed, 386 insertions(+), 96 deletions(-) create mode 100644 docs/walkthrough/addNewDashboardPanel.md create mode 100644 src/components/pages/dashboard/panels/_examplePanel/examplePanel.js create mode 100644 src/components/pages/dashboard/panels/_examplePanel/examplePanel.scss create mode 100644 src/components/pages/dashboard/panels/_examplePanel/index.js diff --git a/docs/walkthrough/README.md b/docs/walkthrough/README.md index 23e414364..03d284bdf 100644 --- a/docs/walkthrough/README.md +++ b/docs/walkthrough/README.md @@ -7,6 +7,7 @@ The following walkthroughs are available to help customize this application. 1. [Adding a New Service](addNewService.md) 1. [Adding a New Grid](addNewGrid.md) 1. [Adding a New Flyout](addNewFlyout.md) +1. [Adding a New Panel to the Dashboard](addNewDashboardPanel.md) ### Show the walkthrough examples in the running application diff --git a/docs/walkthrough/addNewDashboardPanel.md b/docs/walkthrough/addNewDashboardPanel.md new file mode 100644 index 000000000..01ddd0111 --- /dev/null +++ b/docs/walkthrough/addNewDashboardPanel.md @@ -0,0 +1,75 @@ +Walkthrough: Adding a New Panel to the Dashboard +================================================ + +The following is for creating a new panel called "**examplePanel**." + +1. Create a folder named `examplePanel` inside the `components/pages/dashboard/panels` folder. +1. Create 3 files in the new folder. See the individual example files for more details and comments inline. + - [examplePanel.js](/src/components/pages/dashboard/panels/_examplePanel/examplePanel.js) - main component for the panel + - [examplePanel.scss](/src/components/pages/dashboard/panels/_examplePanel/examplePanel.scss) - styles for the new panel + - [index.js](/src/components/pages/dashboard/panels/_examplePanel/index.js) - exports for the new panel +1. Add the new panel to the main panel export file: [dashboard/panels/index.js](/src/components/pages/dashboard/panels/index.js). + ```js + export * from './examplePanel'; + ``` +1. Add the panel header to the translations file, [translations.json](../../public/locales/en/translations.json). [i18next][i18next] is used for internationalization. + ```json + "examplePanel": { + "header": "Example Panel", + }, + ``` +1. In the [examplePanel.js](/src/components/pages/dashboard/panels/_examplePanel/examplePanel.js), import the `Panel` components. + ```js + import { + Panel, + PanelHeader, + PanelHeaderLabel, + PanelContent, + } from 'components/pages/dashboard/panel'; + ``` +1. In the render method, use the various `Panel` components to ensure consistency with others. Then, add whatever components are needed inside `PanelContent`. + ```jsx + + + {t('examples.panel.header')} + + + {t('examples.panel.panelBody')} + + + ``` +1. Add your panel to the [dashboard.js](/src/components/pages/dashboard/dashboard.js) page. Size the `Cell` for the panel according to how much space it will need. See [grid.scss](/src/components/pages/dashboard/grid/grid.scss) for the available grid-cell styles. + ```jsx + + + + ``` +1. **Congratulations!** Run the application and navigate to the Dashboard page. You should see your new panel in action. + +1. Now, you can edit the panel to do what you want. Send props with any data you need. If mapping data and actions from a reducer, consider using the "container" approach described in the [Adding a New Grid](addNewGrid.md) walkthrough. + +1. Optional customizations: + 1. Add an `Indicator` to the header to show pending state. + ```jsx + { isPending && } + ``` + 1. Use a `PanelOverlay` to show pending state. This example uses an `Indicator`, but other components or messages could be placed here. + ```jsx + { isPending && } + ``` + 1. Use `PanelError` and `AjaxError` to show error state. + ```jsx + { error && } + ``` + +### More Information + +- Explore the other remote monitoring [walkthroughs](README.md). +- Technology reference: + - [react][react] + - [i18next][i18next] + + + +[i18next]: https://www.i18next.com/ +[react]: https://reactjs.org/ diff --git a/docs/walkthrough/addNewGrid.md b/docs/walkthrough/addNewGrid.md index 4e5f5b4e6..c153a4108 100644 --- a/docs/walkthrough/addNewGrid.md +++ b/docs/walkthrough/addNewGrid.md @@ -69,7 +69,7 @@ Grids in remote monitoring are based on [ag-grid][ag-grid], with our own customi t: this.props.t }; ``` -1. Add the your grid and `RefreshBar to the `PageContent` (or in another component such as a flyout). +1. Add your grid and `RefreshBar` to the `PageContent` (or in another component such as a flyout). ```jsx diff --git a/docs/walkthrough/addNewService.md b/docs/walkthrough/addNewService.md index 973a2655e..13eaca548 100644 --- a/docs/walkthrough/addNewService.md +++ b/docs/walkthrough/addNewService.md @@ -100,7 +100,7 @@ Services in remote monitoring are called using [rxjs][rxjs] Observables. ]; const rootEpic = combineEpics(...epics); -``` + ``` #### Congratulations! Your service is ready to be hooked up to user interface components. diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 711297647..6dcc44364 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -586,6 +586,10 @@ "close": "Close" } } + }, + "panel": { + "header": "Example Panel", + "panelBody": "This is a new panel." } } } diff --git a/src/components/app/app.js b/src/components/app/app.js index c39dffb3b..0f6393508 100644 --- a/src/components/app/app.js +++ b/src/components/app/app.js @@ -46,6 +46,23 @@ if (Config.showWalkthroughExamples) { tabConfigs.push(gridExampleTab); } +class WalkthroughExampleRoute extends Component { + render() { + const { component: Component, ...props } = this.props + + return ( + ( + Config.showWalkthroughExamples ? + : + + )} + /> + ) + } +} + /** The base component for the app */ class App extends Component { @@ -79,10 +96,10 @@ class App extends Component { - - - + + + {this.props.deviceGroupFlyoutIsOpen && } {this.state.openFlyout === 'settings' && } diff --git a/src/components/pages/dashboard/dashboard.container.js b/src/components/pages/dashboard/dashboard.container.js index 25a4aef98..999c0ef67 100644 --- a/src/components/pages/dashboard/dashboard.container.js +++ b/src/components/pages/dashboard/dashboard.container.js @@ -49,7 +49,8 @@ const mapStateToProps = state => ({ // Wrap the dispatch method const mapDispatchToProps = dispatch => ({ fetchRules: () => dispatch(rulesEpics.actions.fetchRules()), - updateTimeInterval: timeInterval => dispatch(appRedux.actions.updateTimeInterval(timeInterval)) + updateTimeInterval: timeInterval => dispatch(appRedux.actions.updateTimeInterval(timeInterval)), + updateCurrentWindow: (currentWindow) => dispatch(appRedux.actions.updateCurrentWindow(currentWindow)) }); export const DashboardContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Dashboard)); diff --git a/src/components/pages/dashboard/dashboard.js b/src/components/pages/dashboard/dashboard.js index 200af30f9..330b51816 100644 --- a/src/components/pages/dashboard/dashboard.js +++ b/src/components/pages/dashboard/dashboard.js @@ -18,6 +18,7 @@ import { TelemetryPanel, AnalyticsPanel, MapPanel, + ExamplePanel, transformTelemetryResponse, chartColorObjects } from './panels'; @@ -65,6 +66,8 @@ export class Dashboard extends Component { this.dashboardRefresh$ = new Subject(); // Restarts all streams this.telemetryRefresh$ = new Subject(); this.panelsRefresh$ = new Subject(); + + this.props.updateCurrentWindow('Dashboard'); } componentDidMount() { @@ -407,6 +410,11 @@ export class Dashboard extends Component { colors={chartColorObjects} t={t} /> + { Config.showWalkthroughExamples && + + + + } ]; diff --git a/src/components/pages/dashboard/dashboard.test.js b/src/components/pages/dashboard/dashboard.test.js index f88b6d332..404af8e41 100644 --- a/src/components/pages/dashboard/dashboard.test.js +++ b/src/components/pages/dashboard/dashboard.test.js @@ -20,7 +20,8 @@ describe('Dashboard Component', () => { rulesError: undefined, rulesIsPending: false, fetchRules: () => {}, - t: () => {} + t: () => {}, + updateCurrentWindow: () => {} }; const wrapper = shallow( diff --git a/src/components/pages/dashboard/panels/_examplePanel/examplePanel.js b/src/components/pages/dashboard/panels/_examplePanel/examplePanel.js new file mode 100644 index 000000000..96f347026 --- /dev/null +++ b/src/components/pages/dashboard/panels/_examplePanel/examplePanel.js @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; + +import { + Panel, + PanelHeader, + PanelHeaderLabel, + PanelContent, +} from 'components/pages/dashboard/panel'; + +import './examplePanel.css'; + +export class ExamplePanel extends Component { + constructor(props) { + super(props); + + this.state = { isPending: true }; + } + + render() { + const { t } = this.props; + + return ( + + + {t('examples.panel.header')} + + + {t('examples.panel.panelBody')} + + + ); + } +} diff --git a/src/components/pages/dashboard/panels/_examplePanel/examplePanel.scss b/src/components/pages/dashboard/panels/_examplePanel/examplePanel.scss new file mode 100644 index 000000000..e81169e5d --- /dev/null +++ b/src/components/pages/dashboard/panels/_examplePanel/examplePanel.scss @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.example-panel-container { + display: flex; + flex-flow: column nowrap; + padding: 0 !important; + + @include themify($themes) { + color: themed('colorContentTextDim'); + } +} diff --git a/src/components/pages/dashboard/panels/_examplePanel/index.js b/src/components/pages/dashboard/panels/_examplePanel/index.js new file mode 100644 index 000000000..ef0305829 --- /dev/null +++ b/src/components/pages/dashboard/panels/_examplePanel/index.js @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './examplePanel'; diff --git a/src/components/pages/dashboard/panels/index.js b/src/components/pages/dashboard/panels/index.js index 72fa85d73..257a9205d 100644 --- a/src/components/pages/dashboard/panels/index.js +++ b/src/components/pages/dashboard/panels/index.js @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +export * from './_examplePanel'; export * from './alerts'; export * from './overview'; export * from './map'; diff --git a/src/components/pages/devices/devices.container.js b/src/components/pages/devices/devices.container.js index 5c0568a01..3db7b66e1 100644 --- a/src/components/pages/devices/devices.container.js +++ b/src/components/pages/devices/devices.container.js @@ -10,7 +10,11 @@ import { getDevicesLastUpdated, getDevicesPendingStatus } from 'store/reducers/devicesReducer'; -import { getDeviceGroups, getDeviceGroupError } from 'store/reducers/appReducer'; +import { + redux as appRedux, + getDeviceGroups, + getDeviceGroupError +} from 'store/reducers/appReducer'; // Pass the devices status const mapStateToProps = state => ({ @@ -24,7 +28,8 @@ const mapStateToProps = state => ({ // Wrap the dispatch method const mapDispatchToProps = dispatch => ({ - fetchDevices: () => dispatch(devicesEpics.actions.fetchDevices()) + fetchDevices: () => dispatch(devicesEpics.actions.fetchDevices()), + updateCurrentWindow: (currentWindow) => dispatch(appRedux.actions.updateCurrentWindow(currentWindow)) }); export const DevicesContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Devices)); diff --git a/src/components/pages/devices/devices.js b/src/components/pages/devices/devices.js index b4854017f..39e622a0d 100644 --- a/src/components/pages/devices/devices.js +++ b/src/components/pages/devices/devices.js @@ -31,6 +31,8 @@ export class Devices extends Component { ...closedFlyoutState, contextBtns: null }; + + this.props.updateCurrentWindow('Devices'); } componentWillReceiveProps(nextProps) { diff --git a/src/components/pages/devices/devices.test.js b/src/components/pages/devices/devices.test.js index 446038a60..ed778461a 100644 --- a/src/components/pages/devices/devices.test.js +++ b/src/components/pages/devices/devices.test.js @@ -19,6 +19,7 @@ describe('Devices Component', () => { fetchDevices: () => {}, changeDeviceGroup: (id) => {}, t: () => {}, + updateCurrentWindow: () => {} }; const wrapper = shallow( diff --git a/src/components/pages/maintenance/maintenance.container.js b/src/components/pages/maintenance/maintenance.container.js index 7873f4316..1431135f2 100644 --- a/src/components/pages/maintenance/maintenance.container.js +++ b/src/components/pages/maintenance/maintenance.container.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { translate } from 'react-i18next'; import { Maintenance } from './maintenance'; import { + epics as appEpics, redux as appRedux, getTheme, getTimeInterval, @@ -39,7 +40,9 @@ const mapStateToProps = state => ({ // Wrap the dispatch method const mapDispatchToProps = dispatch => ({ fetchRules: () => dispatch(rulesEpics.actions.fetchRules()), - updateTimeInterval: timeInterval => dispatch(appRedux.actions.updateTimeInterval(timeInterval)) + updateTimeInterval: timeInterval => dispatch(appRedux.actions.updateTimeInterval(timeInterval)), + updateCurrentWindow: (currentWindow) => dispatch(appRedux.actions.updateCurrentWindow(currentWindow)), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const MaintenanceContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Maintenance)); diff --git a/src/components/pages/maintenance/maintenance.js b/src/components/pages/maintenance/maintenance.js index 5d4122935..8605cdaad 100644 --- a/src/components/pages/maintenance/maintenance.js +++ b/src/components/pages/maintenance/maintenance.js @@ -48,6 +48,7 @@ export class Maintenance extends Component { this.props.fetchRules(); } + this.props.updateCurrentWindow('Maintenance'); this.subscriptions = []; } @@ -235,7 +236,8 @@ export class Maintenance extends Component { refreshData: this.getData, onTimeIntervalChange: this.onTimeIntervalChange, timeInterval: this.props.timeInterval, - lastUpdated: this.state.lastUpdated + lastUpdated: this.state.lastUpdated, + logEvent: this.props.logEvent }; const alertProps = { isPending: rulesIsPending || alertsIsPending, diff --git a/src/components/pages/maintenance/maintenance.test.js b/src/components/pages/maintenance/maintenance.test.js index 1c5311ba9..57340b8bf 100644 --- a/src/components/pages/maintenance/maintenance.test.js +++ b/src/components/pages/maintenance/maintenance.test.js @@ -16,7 +16,8 @@ describe('Dashboard Component', () => { rulesLastUpdated: undefined, deviceEntities: {}, fetchRules: () => {}, - t: () => {} + t: () => {}, + updateCurrentWindow: () => {} }; const wrapper = shallow( diff --git a/src/components/pages/maintenance/ruleDetails/ruleDetails.js b/src/components/pages/maintenance/ruleDetails/ruleDetails.js index 95c9ae577..8c9429679 100644 --- a/src/components/pages/maintenance/ruleDetails/ruleDetails.js +++ b/src/components/pages/maintenance/ruleDetails/ruleDetails.js @@ -362,7 +362,8 @@ export class RuleDetails extends Component { onHardSelectChange={this.onHardSelectChange('rules')} rowData={rule} pagination={false} - refresh={this.props.fetchRules} /> + refresh={this.props.fetchRules} + logEvent={this.props.logEvent} />

{ t('maintenance.alertOccurrences') }

diff --git a/src/components/pages/rules/flyouts/editRuleFlyout.js b/src/components/pages/rules/flyouts/editRuleFlyout.js index c009a673a..31847bd24 100644 --- a/src/components/pages/rules/flyouts/editRuleFlyout.js +++ b/src/components/pages/rules/flyouts/editRuleFlyout.js @@ -1,29 +1,41 @@ // Copyright (c) Microsoft. All rights reserved. -import React from 'react'; -import { permissions } from 'services/models'; +import React, { Component } from 'react'; +import { permissions, toDiagnosticsModel } from 'services/models'; import { Protected, ProtectedError } from 'components/shared'; import { RuleEditorContainer } from './ruleEditor'; import Flyout from 'components/shared/flyout'; -export const EditRuleFlyout = ({ t, onClose, rule }) => ( - - - {t('rules.flyouts.editRule')} - - - - { - (hasPermission, permission) => - hasPermission - ? - : -
- -

A read-only view will be added soon as part of another PBI.

-
- } -
-
-
-); +export class EditRuleFlyout extends Component { + + onTopXClose = () => { + const { logEvent, onClose } = this.props; + logEvent(toDiagnosticsModel('Rule_TopXCloseClick', {})); + onClose(); + } + + render() { + const { onClose, t, rule } = this.props; + return ( + + + {t('rules.flyouts.editRule')} + + + + { + (hasPermission, permission) => + hasPermission + ? + : +
+ +

A read-only view will be added soon as part of another PBI.

+
+ } +
+
+
+ ); + } +} diff --git a/src/components/pages/rules/flyouts/newRuleFlyout.js b/src/components/pages/rules/flyouts/newRuleFlyout.js index 1bdfb716d..b1aa5bfff 100644 --- a/src/components/pages/rules/flyouts/newRuleFlyout.js +++ b/src/components/pages/rules/flyouts/newRuleFlyout.js @@ -1,21 +1,33 @@ // Copyright (c) Microsoft. All rights reserved. -import React from 'react'; -import { permissions } from 'services/models'; +import React, { Component } from 'react'; +import { permissions, toDiagnosticsModel } from 'services/models'; import { Protected } from 'components/shared'; import { RuleEditorContainer } from './ruleEditor'; import Flyout from 'components/shared/flyout'; -export const NewRuleFlyout = ({ t, onClose }) => ( - - - {t('rules.flyouts.newRule')} - - - - - - - - -); +export class NewRuleFlyout extends Component { + + onTopXClose = () => { + const { logEvent, onClose } = this.props; + logEvent(toDiagnosticsModel('Rule_TopXCloseClick', {})); + onClose(); + } + + render () { + const { onClose, t } = this.props; + return ( + + + {t('rules.flyouts.newRule')} + + + + + + + + + ); + } +} diff --git a/src/components/pages/rules/flyouts/ruleDetailsFlyout.js b/src/components/pages/rules/flyouts/ruleDetailsFlyout.js index ba96330ec..d7afa2f9f 100644 --- a/src/components/pages/rules/flyouts/ruleDetailsFlyout.js +++ b/src/components/pages/rules/flyouts/ruleDetailsFlyout.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { svgs } from 'utilities'; -import { permissions } from 'services/models'; +import { permissions, toDiagnosticsModel } from 'services/models'; import { Btn, Protected, ProtectedError } from 'components/shared'; import { RuleEditorContainer } from './ruleEditor'; import { RuleViewerContainer } from './ruleViewer'; @@ -26,7 +26,18 @@ export class RuleDetailsFlyout extends Component { } } - goToEditMode = () => { this.setState({ isEditable: true }); } + onTopXClose = () => { + const { logEvent, onClose } = this.props; + if(this.state.isEditable) { + logEvent(toDiagnosticsModel('Rule_TopXCloseClick', {})); + } + onClose(); + } + + goToEditMode = () => { + this.props.logEvent(toDiagnosticsModel('Rule_EditClick', {})); + this.setState({ isEditable: true }); + } render() { const { t, onClose, rule } = this.props; @@ -36,7 +47,7 @@ export class RuleDetailsFlyout extends Component { {isEditable ? t('rules.flyouts.editRule') : t('rules.flyouts.viewRule')} - + {!isEditable diff --git a/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js b/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js index 0209ce747..ff4a9cab1 100644 --- a/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js +++ b/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js @@ -32,7 +32,9 @@ import { ruleCalculations, ruleTimePeriods, ruleOperators, - toRuleDiagnosticsModel + toRuleDiagnosticsModel, + toDiagnosticsModel, + toSinglePropertyDiagnosticsModel } from 'services/models'; import Config from 'app.config'; @@ -108,7 +110,10 @@ export class RuleEditor extends LinkedComponent { toSelectOption = ({ id, displayName }) => ({ label: displayName, value: id }); - addCondition = () => this.conditionsLink.set([...this.conditionsLink.value, newCondition()]); + addCondition = () => { + this.conditionsLink.set([...this.conditionsLink.value, newCondition()]); + this.props.logEvent(toDiagnosticsModel('Rule_AddConditionClick', {})); + } deleteCondition = (index) => (evt) => this.conditionsLink.set(this.conditionsLink.value.filter((_, idx) => index !== idx)); @@ -160,16 +165,18 @@ export class RuleEditor extends LinkedComponent { }, error => this.setState({ error, isPending: false, changesApplied: true }) ); - logEvent(toRuleDiagnosticsModel('RuleApplyClick', requestProps)); + logEvent(toRuleDiagnosticsModel('Rule_ApplyClick', requestProps)); } } } onGroupIdChange = ({ target: { value: { value = {} } } }) => { + const { logEvent } = this.props; this.setState({ fieldQueryPending: true, isPending: true }); + logEvent(toSinglePropertyDiagnosticsModel('Rule_DeviceGroupClick', 'DeviceGroup', value)); this.getDeviceCountAndFields(value); this.formControlChange(); } @@ -209,11 +216,41 @@ export class RuleEditor extends LinkedComponent { } //todo toggle button didn't support link - onToggle = ({ target: { value } }) => { - this.setState({ formData: { ...this.state.formData, enabled: value } }) + onStatusToggle = ({ target: { value } }) => { + this.setState({ formData: { ...this.state.formData, enabled: value } }); + this.props.logEvent(toSinglePropertyDiagnosticsModel('Rule_StatusToggle', 'RuleStatus', value ? 'Enabled' : 'Disabled')); + this.formControlChange(); + } + + onCalculationChange = ({ target: { value: { value = {} } } }) => { + this.props.logEvent(toSinglePropertyDiagnosticsModel('Rule_CalculationClick', 'Calculation', value)); + this.formControlChange(); + } + + onFieldChange = (index, { target: { value: { value = {} } } }) => { + var eventProperties = { 'FieldChosen': value, 'ConditionNumber': index }; + this.props.logEvent(toDiagnosticsModel('Rule_FieldClick', eventProperties)); + this.formControlChange(); + } + + onOperatorChange = (index, { target: { value: { value = {} } } }) => { + var eventProperties = { 'OperatorChosen': value, 'ConditionNumber': index }; + this.props.logEvent(toDiagnosticsModel('Rule_OperatorClick', eventProperties)); this.formControlChange(); } + onSeverityChange = ({ target: { value } }) => { + this.props.logEvent(toSinglePropertyDiagnosticsModel('Rule_SeverityLevelClick', 'SeverityLevel', value)) + this.formControlChange(); + } + + onCloseClick = () => { + const { onClose, logEvent } = this.props; + const rule = { ...this.state.formData }; + logEvent(toRuleDiagnosticsModel('Rule_CancelClick', rule)); + onClose(); + } + formControlChange = () => { if (this.state.changesApplied) { this.setState({ changesApplied: false }); @@ -221,7 +258,7 @@ export class RuleEditor extends LinkedComponent { } render() { - const { onClose, t, deviceGroups = [] } = this.props; + const { t, deviceGroups = [] } = this.props; const { changesApplied, devicesAffected, @@ -311,7 +348,7 @@ export class RuleEditor extends LinkedComponent { placeholder={t('rules.flyouts.ruleEditor.calculationPlaceholder')} link={this.calculationLink} options={calculationOptions} - onChange={this.formControlChange} + onChange={this.onCalculationChange} clearable={false} searchable={false} /> @@ -350,7 +387,7 @@ export class RuleEditor extends LinkedComponent { type="select" className="long" placeholder={t('rules.flyouts.ruleEditor.condition.fieldPlaceholder')} - onChange={this.formControlChange} + onChange={(target) => this.onFieldChange(idx + 1, target)} link={condition.fieldLink} options={fieldOptions} clearable={false} @@ -362,7 +399,7 @@ export class RuleEditor extends LinkedComponent { type="select" className="short" placeholder={t('rules.flyouts.ruleEditor.condition.operatorPlaceholder')} - onChange={this.formControlChange} + onChange={(target) => this.onOperatorChange(idx + 1, target)} link={condition.operatorLink} options={ruleOperators} clearable={false} @@ -389,7 +426,7 @@ export class RuleEditor extends LinkedComponent { {t('rules.flyouts.ruleEditor.severityLevel')} @@ -397,7 +434,7 @@ export class RuleEditor extends LinkedComponent { @@ -405,7 +442,7 @@ export class RuleEditor extends LinkedComponent { @@ -419,7 +456,7 @@ export class RuleEditor extends LinkedComponent { {t('rules.flyouts.ruleEditor.ruleStatus')} + onChange={this.onStatusToggle} > {formData.enabled ? t('rules.flyouts.ruleEditor.ruleEnabled') : t('rules.flyouts.ruleEditor.ruleDisabled')} @@ -442,7 +479,7 @@ export class RuleEditor extends LinkedComponent { { {t('rules.flyouts.ruleEditor.apply')} - {t('rules.flyouts.ruleEditor.cancel')} + {t('rules.flyouts.ruleEditor.cancel')} } diff --git a/src/components/pages/rules/rules.container.js b/src/components/pages/rules/rules.container.js index 7d48926a4..c75b656db 100644 --- a/src/components/pages/rules/rules.container.js +++ b/src/components/pages/rules/rules.container.js @@ -11,7 +11,11 @@ import { getRulesLastUpdated, getRulesPendingStatus } from 'store/reducers/rulesReducer'; -import { getDeviceGroups } from 'store/reducers/appReducer'; +import { + epics as appEpics, + redux as appRedux, + getDeviceGroups +} from 'store/reducers/appReducer'; // Pass the devices status const mapStateToProps = state => ({ @@ -25,7 +29,9 @@ const mapStateToProps = state => ({ // Wrap the dispatch method const mapDispatchToProps = dispatch => ({ - fetchRules: () => dispatch(rulesEpics.actions.fetchRules()) + fetchRules: () => dispatch(rulesEpics.actions.fetchRules()), + updateCurrentWindow: (currentWindow) => dispatch(appRedux.actions.updateCurrentWindow(currentWindow)), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const RulesContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Rules)); diff --git a/src/components/pages/rules/rules.js b/src/components/pages/rules/rules.js index 3997511b9..bea5ef561 100644 --- a/src/components/pages/rules/rules.js +++ b/src/components/pages/rules/rules.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import React, { Component } from 'react'; -import { permissions } from 'services/models'; +import { permissions, toDiagnosticsModel } from 'services/models'; import { RulesGrid } from './rulesGrid'; import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/app/deviceGroupDropdown'; import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn'; @@ -36,6 +36,8 @@ export class Rules extends Component { if (!this.props.lastUpdated && !this.props.error) { this.props.fetchRules(); } + + this.props.updateCurrentWindow('Rules'); } componentWillReceiveProps(nextProps) { @@ -52,10 +54,14 @@ export class Rules extends Component { closeFlyout = () => this.setState(closedFlyoutState); - openNewRuleFlyout = () => this.setState({ - openFlyoutName: 'newRule', - selectedRuleId: '' - }); + openNewRuleFlyout = () => { + const { logEvent } = this.props; + this.setState({ + openFlyoutName: 'newRule', + selectedRuleId: '' + }); + logEvent(toDiagnosticsModel('Rule_NewClick', {})); + } onGridReady = gridReadyEvent => this.rulesGridApi = gridReadyEvent.api; @@ -72,7 +78,8 @@ export class Rules extends Component { error, isPending, lastUpdated, - fetchRules + fetchRules, + logEvent } = this.props; const gridProps = { onGridReady: this.onGridReady, @@ -80,7 +87,8 @@ export class Rules extends Component { onContextMenuChange: this.onContextMenuChange, t: this.props.t, deviceGroups: this.props.deviceGroups, - refresh: fetchRules + refresh: fetchRules, + logEvent: this.props.logEvent }; return [ @@ -96,7 +104,7 @@ export class Rules extends Component { { !!error && } {!error && } - {this.state.openFlyoutName === 'newRule' && } + {this.state.openFlyoutName === 'newRule' && }
]; } diff --git a/src/components/pages/rules/rules.test.js b/src/components/pages/rules/rules.test.js index 72ac6164a..163294d17 100644 --- a/src/components/pages/rules/rules.test.js +++ b/src/components/pages/rules/rules.test.js @@ -19,7 +19,8 @@ describe('Rules Component', () => { lastUpdated: undefined, fetchRules: () => {}, changeDeviceGroup: (id) => {}, - t: () => {} + t: () => {}, + updateCurrentWindow: () => {} }; const wrapper = shallow( diff --git a/src/components/pages/rules/rulesGrid/rulesGrid.js b/src/components/pages/rules/rulesGrid/rulesGrid.js index d375c20c5..5b6d03ecf 100644 --- a/src/components/pages/rules/rulesGrid/rulesGrid.js +++ b/src/components/pages/rules/rulesGrid/rulesGrid.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { Trans } from 'react-i18next'; -import { permissions } from 'services/models'; +import { permissions, toDiagnosticsModel } from 'services/models'; import { Btn, PcsGrid, Protected } from 'components/shared'; import { rulesColumnDefs, defaultRulesGridProps } from './rulesGridConfig'; import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig'; @@ -93,7 +93,10 @@ export class RulesGrid extends Component { } } - openEditRuleFlyout = () => this.setState({ openFlyoutName: 'edit' }); + openEditRuleFlyout = () => { + this.props.logEvent(toDiagnosticsModel('Rule_EditClick', {})); + this.setState({ openFlyoutName: 'edit' }); + } openStatusFlyout = () => this.setState({ openFlyoutName: 'status' }); @@ -104,9 +107,9 @@ export class RulesGrid extends Component { getOpenFlyout = () => { switch (this.state.openFlyoutName) { case 'view': - return + return case 'edit': - return + return case 'status': return case 'delete': diff --git a/src/services/models/diagnosticsModels.js b/src/services/models/diagnosticsModels.js index b38e9dae4..b02dfb2fb 100644 --- a/src/services/models/diagnosticsModels.js +++ b/src/services/models/diagnosticsModels.js @@ -2,7 +2,8 @@ export const toDiagnosticsRequestModel = (request = {}) => ({ EventType: request.eventType, - EventProperties: request.eventProperties + EventProperties: request.eventProperties, + SessionId: request.sessionId }); export const toDiagnosticsModel = (eventType, eventProperties) => ({ diff --git a/src/services/models/logEventModels.js b/src/services/models/logEventModels.js index 5ebb4aad5..9ece4eec8 100644 --- a/src/services/models/logEventModels.js +++ b/src/services/models/logEventModels.js @@ -2,11 +2,22 @@ import { toDiagnosticsModel } from 'services/models'; -export const toLogRuleModel = (rule = {}) => ({ - 'DeviceGroup': rule.groupId, - 'Calculation': rule.calculation, - 'TimePeriod': rule.timePeriod, - 'Severity': rule.severity -}); - -export const toRuleDiagnosticsModel = (eventName, rule) => toDiagnosticsModel(eventName, toLogRuleModel(rule)); +export const toRuleDiagnosticsModel = (eventName, rule) => +{ + const metadata = { + DeviceGroup: rule.groupId, + Calculation : rule.calculation, + TimePeriod: rule.timePeriod, + SeverityLevel: rule.severity, + ConditionCount: rule.conditions.length, + FirstFieldChosen: rule.conditions[0].field, + FirstOperatorChosen: rule.conditions[0].operator + }; + + return toDiagnosticsModel(eventName, metadata); +} + +export const toSinglePropertyDiagnosticsModel = (eventName, propertyTitle, property) => { + const metadata = { [propertyTitle]: property }; + return toDiagnosticsModel(eventName, metadata); +} diff --git a/src/store/reducers/appReducer.js b/src/store/reducers/appReducer.js index 6abd165a7..d8588ffec 100644 --- a/src/store/reducers/appReducer.js +++ b/src/store/reducers/appReducer.js @@ -3,6 +3,7 @@ import 'rxjs'; import { Observable } from 'rxjs'; import { AuthService, ConfigService, GitHubService, DiagnosticsService } from 'services'; +import moment from 'moment'; import { schema, normalize } from 'normalizr'; import { createSelector } from 'reselect'; import update from 'immutability-helper'; @@ -43,10 +44,12 @@ export const epics = createEpicScenario({ epic: ({ payload }, store) => { const diagnosticsOptIn = getDiagnosticsOptIn(store.getState()); if (diagnosticsOptIn) { + payload.sessionId = getSessionId(store.getState()); + payload.eventProperties.CurrentWindow = getCurrentWindow(store.getState()); return DiagnosticsService.logEvent(payload) /* We don't want anymore action to be executed after this call - and hence return empty observable */ - .map(_ => Observable.empty()) + and hence return empty observable in flatMap */ + .flatMap(_ => Observable.empty()) .catch(_ => Observable.empty()) } else { return Observable.empty() @@ -142,6 +145,7 @@ export const epics = createEpicScenario({ .map(toActionCreator(redux.actions.getReleaseInformation, fromAction)) .catch(handleError(fromAction)) } + }); // ========================= Epics - END @@ -170,7 +174,9 @@ const initialState = { name: '', diagnosticsOptIn: true }, - userPermissions: new Set() + userPermissions: new Set(), + sessionId: moment().utc().unix(), + currentWindow: '' }; const updateUserReducer = (state, { payload, fromAction }) => { @@ -232,6 +238,10 @@ const setDeviceGroupFlyoutReducer = (state, { payload }) => update(state, { deviceGroupFlyoutIsOpen: { $set: !!payload } }); +const updateCurrentWindow = (state, { payload }) => update(state, + { currentWindow: { $set: payload } } +); + /* Action types that cause a pending flag */ const fetchableTypes = [ epics.actionTypes.fetchDeviceGroups, @@ -254,7 +264,8 @@ export const redux = createReducerScenario({ updateSolutionSettings: { type: 'APP_UPDATE_SOLUTION_SETTINGS', reducer: updateSolutionSettingsReducer }, getReleaseInformation: { type: 'APP_GET_VERSION', reducer: releaseReducer }, setDeviceGroupFlyoutStatus: { type: 'APP_SET_DEVICE_GROUP_FLYOUT_STATUS', reducer: setDeviceGroupFlyoutReducer }, - updateTimeInterval: { type: 'APP_UPDATE_TIME_INTERVAL', reducer: updateTimeInterval } + updateTimeInterval: { type: 'APP_UPDATE_TIME_INTERVAL', reducer: updateTimeInterval }, + updateCurrentWindow: { type: 'APP_UPDATE_CURRENT_WINDOW', reducer: updateCurrentWindow } }); export const reducer = { app: redux.getReducer(initialState) }; @@ -308,4 +319,6 @@ export const getLogoPendingStatus = state => export const getTimeInterval = state => getAppReducer(state).timeInterval; export const getUserPermissions = state => getAppReducer(state).userPermissions; +export const getSessionId = state => getAppReducer(state).sessionId; +export const getCurrentWindow = state => getAppReducer(state).currentWindow // ========================= Selectors - END From 3f2fc8fad765048fbe76581fb3ce5ddafeda1de3 Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 24 Aug 2018 13:55:03 -0700 Subject: [PATCH 04/25] Add newPackage Flyout (#1070) * newPackage commit 1 * onChange * upload successful * add new package to grid * newPackage complete * alignment * remove color inside theme * combine * .new-package-header combine * error message * single delete --- public/locales/en/translations.json | 19 ++- src/assets/icons/deployments.svg | 4 + src/assets/icons/packages.svg | 4 +- src/components/app/app.js | 2 +- .../packages/flyouts/newPackage/index.js | 1 + .../newPackage/newPackage.container.js | 16 +- .../packages/flyouts/newPackage/newPackage.js | 154 +++++++++++++++++- .../flyouts/newPackage/newPackage.scss | 50 +++++- src/components/pages/packages/packages.js | 4 +- src/services/configService.js | 15 +- src/services/httpClient.js | 8 +- src/services/models/configModels.js | 14 +- src/store/reducers/packagesReducer.js | 37 ++++- 13 files changed, 294 insertions(+), 34 deletions(-) create mode 100644 src/assets/icons/deployments.svg diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 6dcc44364..86cd1aab4 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -556,8 +556,25 @@ }, "flyouts": { "new": { - "title": "New Package" + "title": "New Package", + "header": "Upload a package", + "description": "Add a package to your solution", + "upload": "Upload", + "cancel": "Cancel", + "close": "Close", + "type": "Package type", + "browse": "Browse", + "browseText": "for a package file", + "placeHolder": "Select package type", + "package": "Package", + "deploymentText":"To deploy packages, go to the 'Deployments' page and click '+ New Deployment' button.", + "validation": { + "required": "Is required" + } } + }, + "typeOptions": { + "edge_manifest": "Edge Manifest" } }, "examples": { diff --git a/src/assets/icons/deployments.svg b/src/assets/icons/deployments.svg new file mode 100644 index 000000000..96b32c6c1 --- /dev/null +++ b/src/assets/icons/deployments.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/packages.svg b/src/assets/icons/packages.svg index 96b32c6c1..47a752cd9 100644 --- a/src/assets/icons/packages.svg +++ b/src/assets/icons/packages.svg @@ -1,4 +1,4 @@ - - + + diff --git a/src/components/app/app.js b/src/components/app/app.js index 0f6393508..002842633 100644 --- a/src/components/app/app.js +++ b/src/components/app/app.js @@ -37,7 +37,7 @@ const packagesTab = { to: '/packages', svg: svgs.tabs.packages, labelId: 'tabs.p const exampleTab = { to: '/example', svg: svgs.tabs.example, labelId: 'tabs.example' }; const flyoutExampleTab = { to: '/flyoutexample', svg: svgs.tabs.example, labelId: 'tabs.flyoutExample' }; const gridExampleTab = { to: '/gridexample', svg: svgs.tabs.example, labelId: 'tabs.gridExample' }; -const tabConfigs = [dashboardTab, devicesTab, rulesTab, maintenanceTab, packagesTab]; +const tabConfigs = [dashboardTab, devicesTab, rulesTab, packagesTab, maintenanceTab]; /** Only show example pages and components when configured to do so */ if (Config.showWalkthroughExamples) { diff --git a/src/components/pages/packages/flyouts/newPackage/index.js b/src/components/pages/packages/flyouts/newPackage/index.js index 485cef754..0490c9475 100644 --- a/src/components/pages/packages/flyouts/newPackage/index.js +++ b/src/components/pages/packages/flyouts/newPackage/index.js @@ -1,3 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. export * from './newPackage'; +export * from './newPackage.container'; diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.container.js b/src/components/pages/packages/flyouts/newPackage/newPackage.container.js index b62f8092e..7b8fa64c7 100644 --- a/src/components/pages/packages/flyouts/newPackage/newPackage.container.js +++ b/src/components/pages/packages/flyouts/newPackage/newPackage.container.js @@ -3,11 +3,21 @@ import { connect } from 'react-redux'; import { translate } from 'react-i18next'; import { NewPackage } from './newPackage'; -import { epics as packagesEpics } from 'store/reducers/packagesReducer'; +import { + getCreatePackageError, + getCreatePackagePendingStatus, + epics as packagesEpics +} from 'store/reducers/packagesReducer'; + +// Pass the global info needed +const mapStateToProps = state => ({ + isPending: getCreatePackagePendingStatus(state), + error: getCreatePackageError(state) +}); // Wrap the dispatch methods const mapDispatchToProps = dispatch => ({ - createPackage: (packageObj) => dispatch(packagesEpics.actions.createPackage(packageObj)) + createPackage: packageModel => dispatch(packagesEpics.actions.createPackage(packageModel)) }); -export const NewPackageContainer = translate()(connect(null, mapDispatchToProps)(NewPackage)); +export const NewPackageContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(NewPackage)); diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.js b/src/components/pages/packages/flyouts/newPackage/newPackage.js index 37e3ace42..a7c89185e 100644 --- a/src/components/pages/packages/flyouts/newPackage/newPackage.js +++ b/src/components/pages/packages/flyouts/newPackage/newPackage.js @@ -1,13 +1,161 @@ // Copyright (c) Microsoft. All rights reserved. -import { LinkedComponent } from 'utilities'; +import React from 'react'; + +import { packageTypeOptions } from 'services/models'; +import { svgs, LinkedComponent, Validator } from 'utilities'; +import { + AjaxError, + Btn, + BtnToolbar, + Flyout, + FlyoutHeader, + FlyoutTitle, + FlyoutCloseBtn, + FlyoutContent, + Indicator, + FormControl, + FormGroup, + FormLabel, + SummaryBody, + SectionDesc, + SummaryCount, + SummarySection, + Svg +} from 'components/shared'; import './newPackage.css'; +const fileInputAccept = ".json,application/json"; + export class NewPackage extends LinkedComponent { - // TODO + constructor(props) { + super(props); + + this.state = { + type: undefined, + packageFile: undefined, + changesApplied: undefined + }; + } + + apply = (event) => { + event.preventDefault(); + const { createPackage } = this.props; + const { type, packageFile } = this.state; + if (this.formIsValid()) { + createPackage({ type: type, packageFile: packageFile }); + this.setState({ changesApplied: true }); + } + } + + onFileSelected = (e) => { + let file = e.target.files[0]; + this.setState({ packageFile: file }); + } + + formIsValid = () => { + return [ + this.packageTypeLink, + ].every(link => !link.error); + } render() { - return ([]); + const { t, onClose, isPending, error } = this.props; + const { packageFile, changesApplied } = this.state; + + const summaryCount = 1; + const typeOptions = packageTypeOptions.map(value => ({ + label: t(`packages.typeOptions.${value.toLowerCase()}`), + value + })); + + const completedSuccessfully = changesApplied && !error && !isPending; + // Validators + const requiredValidator = (new Validator()).check(Validator.notEmpty, t('packages.flyouts.new.validation.required')); + + // Links + this.packageTypeLink = this.linkTo('type').map(({ value }) => value).withValidator(requiredValidator); + + return ( + + + {t('packages.flyouts.new.title')} + + + +
+
{t('packages.flyouts.new.header')}
+
{t('packages.flyouts.new.description')}
+ + + {t('packages.flyouts.new.type')} + + + +
+ + + {t('packages.flyouts.new.browseText')} +
+ + + + {packageFile && {summaryCount}} + {packageFile && {t('packages.flyouts.new.package')}} + {isPending && } + {completedSuccessfully && } + + {packageFile &&
{packageFile.name}
} + { + completedSuccessfully && +
+ {t('packages.flyouts.new.deploymentText')} +
+ } + {/** Displays an error message if one occurs while applying changes. */ + error && + } + { + /** If package is selected, show the buttons for uploading and closing the flyout. */ + (packageFile && !completedSuccessfully) && + + {t('packages.flyouts.new.upload')} + {t('packages.flyouts.new.cancel')} + + } + { + /** If package is not selected, show only the cancel button. */ + (!packageFile) && + + {t('packages.flyouts.new.cancel')} + + } + { + /** After successful upload, show close button. */ + (completedSuccessfully) && + + {t('packages.flyouts.new.close')} + + } +
+ + + + ); } } diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.scss b/src/components/pages/packages/flyouts/newPackage/newPackage.scss index 02febbecd..8ded51940 100644 --- a/src/components/pages/packages/flyouts/newPackage/newPackage.scss +++ b/src/components/pages/packages/flyouts/newPackage/newPackage.scss @@ -1,3 +1,51 @@ // Copyright (c) Microsoft. All rights reserved. -// TODO - Work in progress +@import 'src/styles/variables'; +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.new-package-content { + .new-package-upload-container { + display: flex; + align-items: flex-start; + word-wrap: break-word; + flex-grow: 0; + @include rem-fallback(padding-top, 24px); + @include rem-fallback(padding-bottom, 24px); + } + + .new-package-browse-click { + cursor: pointer; + text-decoration: underline; + @include rem-fallback(padding-right, 8px); + } + + .new-package-hidden-input { display: none; } + + .new-package-file-name, + .new-package-deployment-text, + .new-package-header, + .new-package-flyout-error { @include rem-fallback(padding-top, 24px); } + + .new-package-descr { + @include rem-fallback(font-size, 12px); + @include rem-fallback(padding-top, 16px); + @include rem-fallback(padding-bottom, 24px); + } + + .summary-icon svg { + @include square-px-rem(16px); + @include rem-fallback(margin-left, 8px); + } + + @include themify($themes) { + .new-package-header, + .new-package-browse-click, + .new-package-file-name { color: themed('colorContentText'); } + + .new-package-descr svg, + .summary-icon svg { fill: themed('colorContentText'); } + + .new-package-flyout-error { border-color: themed('colorAlert'); } + } +} diff --git a/src/components/pages/packages/packages.js b/src/components/pages/packages/packages.js index e72837b76..5710a2cd4 100644 --- a/src/components/pages/packages/packages.js +++ b/src/components/pages/packages/packages.js @@ -12,7 +12,7 @@ import { RefreshBar, PageTitle } from 'components/shared'; -import { NewPackage } from './flyouts'; +import { NewPackageContainer } from './flyouts'; import { svgs } from 'utilities'; import './packages.css'; @@ -73,7 +73,7 @@ export class Packages extends Component { {!!error && } {!error && } - {this.state.openFlyoutName === 'newPackage' && } + {this.state.openFlyoutName === 'new-Package' && } ]; } diff --git a/src/services/configService.js b/src/services/configService.js index 579ccd744..d01b3d61a 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -8,7 +8,8 @@ import { toDeviceGroupsModel, toSolutionSettingThemeModel, toNewPackageRequestModel, - toPackagesModel + toPackagesModel, + toPackageModel } from './models'; import { Observable } from '../../node_modules/rxjs'; @@ -89,9 +90,15 @@ export class ConfigService { } /** Creates a new package */ - static createPackage(payload) { - return HttpClient.post(`${ENDPOINT}packages`, payload) - .map(toNewPackageRequestModel); + static createPackage(packageModel) { + var options = { + headers: { + 'Accept': undefined, + 'Content-Type': undefined + } + } + return HttpClient.post(`${ENDPOINT}packages`, toNewPackageRequestModel(packageModel), options) + .map(toPackageModel); } /** Returns all the account's packages */ diff --git a/src/services/httpClient.js b/src/services/httpClient.js index efe6a7ef8..142d370b8 100644 --- a/src/services/httpClient.js +++ b/src/services/httpClient.js @@ -89,14 +89,18 @@ export class HttpClient { if (accessToken) authHeaders['Authorization'] = `Bearer ${accessToken}`; }); } - return { + const options = { ...request, headers: { ...jsonHeaders, ...request.headers, ...authHeaders } - }; + } + if (options.headers && options.headers.hasOwnProperty('Content-Type') && options.headers['Content-Type'] === undefined) { + delete options.headers['Content-Type']; + } + return options; } /** diff --git a/src/services/models/configModels.js b/src/services/models/configModels.js index 236ff8c77..b5a6580f3 100644 --- a/src/services/models/configModels.js +++ b/src/services/models/configModels.js @@ -32,7 +32,7 @@ export const toUpdateDeviceGroupRequestModel = (params = {}) => ({ })) }); -export const prepareLogoResponse = ({ xhr, response }) => { +export const prepareLogoResponse = ({ xhr, response }) => { const returnObj = {}; const isDefault = xhr.getResponseHeader('IsDefault'); if (!stringToBoolean(isDefault)) { @@ -54,14 +54,16 @@ export const toSolutionSettingThemeModel = (response = {}) => camelCaseReshape(r 'azureMapsKey': 'azureMapsKey' }); +export const packageTypeOptions = ['EDGE_MANIFEST']; + export const toNewPackageRequestModel = ({ type, - packageObj + packageFile }) => { - return { - Type: type, - Package: packageObj - }; + const data = new FormData(); + data.append('Type', type); + data.append('Package', packageFile); + return data; } export const toPackagesModel = (response = {}) => getItems(response) diff --git a/src/store/reducers/packagesReducer.js b/src/store/reducers/packagesReducer.js index 75d4a1e7b..e2ad48fdf 100644 --- a/src/store/reducers/packagesReducer.js +++ b/src/store/reducers/packagesReducer.js @@ -40,8 +40,8 @@ export const epics = createEpicScenario({ .map(toActionCreator(redux.actions.insertPackage, fromAction)) .catch(handleError(fromAction)) }, - /** Delete packages */ - deletePackages: { + /** Delete package */ + deletePackage: { type: 'PACKAGES_DELETE', epic: fromAction => ConfigService.deletePackage(fromAction.payload) @@ -59,15 +59,23 @@ const packageListSchema = new schema.Array(packageSchema); // ========================= Reducers - START const initialState = { ...errorPendingInitialState, entities: {} }; -const insertPackageReducer = (state, { payload }) => { - const { entities: { packages }, result } = normalize(payload, packageListSchema); +const insertPackageReducer = (state, { payload, fromAction }) => { + const { entities: { packages }, result } = normalize(payload, packageSchema); + if (state.entities) { + return update(state, { + entities: { $merge: packages }, + items: { $splice: [[state.items.length, 0, result]] }, + ...setPending(fromAction.type, false) + }); + } return update(state, { - entities: { $merge: packages }, - items: { $splice: [[state.items.length, 0, result]] } + entities: { $set: packages }, + items: { $set: [result] }, + ...setPending(fromAction.type, false) }); }; -const deletePackagesReducer = (state, { payload }) => { +const deletePackagesReducer = (state, { payload, fromAction }) => { const spliceArr = payload.reduce((idxAcc, payloadItem) => { const idx = state.items.indexOf(payloadItem); if (idx !== -1) { @@ -77,7 +85,8 @@ const deletePackagesReducer = (state, { payload }) => { }, []); return update(state, { entities: { $unset: payload }, - items: { $splice: spliceArr } + items: { $splice: spliceArr }, + ...setPending(fromAction.type, false) }); }; @@ -93,7 +102,9 @@ const updatePackagesReducer = (state, { payload, fromAction }) => { /* Action types that cause a pending flag */ const fetchableTypes = [ - epics.actionTypes.fetchPackages + epics.actionTypes.fetchPackages, + epics.actionTypes.createPackage, + epics.actionTypes.deletePackage ]; export const redux = createReducerScenario({ @@ -116,6 +127,14 @@ export const getPackagesError = state => getError(getPackagesReducer(state), epics.actionTypes.fetchPackages); export const getPackagesPendingStatus = state => getPending(getPackagesReducer(state), epics.actionTypes.fetchPackages); +export const getCreatePackageError = state => + getError(getPackagesReducer(state), epics.actionTypes.createPackage); +export const getCreatePackagePendingStatus = state => + getPending(getPackagesReducer(state), epics.actionTypes.createPackage); +export const getDeletePackageError = state => + getError(getPackagesReducer(state), epics.actionTypes.deletePackage); +export const getDeletePackagePendingStatus = state => + getPending(getPackagesReducer(state), epics.actionTypes.deletePackage); export const getPackages = createSelector( getEntities, getItems, (entities, items) => items.map(id => entities[id]) From 9d89d94e1092dd6cbcce700213b2018fe0d32bce Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 31 Aug 2018 15:52:46 -0700 Subject: [PATCH 05/25] Delete package (#1076) * newPackage commit 1 * onChange * upload successful * add new package to grid * newPackage complete * alignment * remove color inside theme * combine * .new-package-header combine * error message * single delete * deletePackage commit 1 * deletePackage commit 2 * delete commit 3 * delete commit 4 * README.md * ME's review comments fix * Fix for Elvin's Comments * fix for stephen's comments --- public/locales/en/translations.json | 10 ++- .../deletePackage/deletePackage.container.js | 23 +++++++ .../flyouts/deletePackage/deletePackage.js | 65 +++++++++++++++++++ .../flyouts/deletePackage/deletePackage.scss | 36 ++++++++++ .../packages/flyouts/deletePackage/index.js | 4 ++ .../pages/packages/flyouts/index.js | 1 + .../packages/flyouts/newPackage/newPackage.js | 2 +- .../packages/packagesGrid/packagesGrid.js | 23 +++++-- src/components/shared/index.js | 1 + src/components/shared/modal/README.md | 24 +++++++ src/components/shared/modal/index.js | 5 ++ src/components/shared/modal/modal/modal.js | 47 ++++++++++++++ src/components/shared/modal/modal/modal.scss | 17 +++++ .../shared/modal/modalContent/modalContent.js | 12 ++++ .../modal/modalContent/modalContent.scss | 20 ++++++ .../shared/modal/modalFadeBox/modalFadeBox.js | 12 ++++ .../modal/modalFadeBox/modalFadeBox.scss | 18 +++++ src/services/models/configModels.js | 2 +- src/store/reducers/packagesReducer.js | 16 ++--- src/styles/_themes.scss | 14 ++++ 20 files changed, 333 insertions(+), 19 deletions(-) create mode 100644 src/components/pages/packages/flyouts/deletePackage/deletePackage.container.js create mode 100644 src/components/pages/packages/flyouts/deletePackage/deletePackage.js create mode 100644 src/components/pages/packages/flyouts/deletePackage/deletePackage.scss create mode 100644 src/components/pages/packages/flyouts/deletePackage/index.js create mode 100644 src/components/shared/modal/README.md create mode 100644 src/components/shared/modal/index.js create mode 100644 src/components/shared/modal/modal/modal.js create mode 100644 src/components/shared/modal/modal/modal.scss create mode 100644 src/components/shared/modal/modalContent/modalContent.js create mode 100644 src/components/shared/modal/modalContent/modalContent.scss create mode 100644 src/components/shared/modal/modalFadeBox/modalFadeBox.js create mode 100644 src/components/shared/modal/modalFadeBox/modalFadeBox.scss diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 86cd1aab4..5f741db95 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -567,14 +567,20 @@ "browseText": "for a package file", "placeHolder": "Select package type", "package": "Package", - "deploymentText":"To deploy packages, go to the 'Deployments' page and click '+ New Deployment' button.", + "deploymentText": "To deploy packages, go to the 'Deployments' page and click '+ New Deployment' button.", "validation": { "required": "Is required" } + }, + "delete": { + "title": "Delete Package?", + "delete": "Delete", + "cancel": "cancel", + "info": "Deleting selected package will remove it. It will not impact any of the deployments of this package." } }, "typeOptions": { - "edge_manifest": "Edge Manifest" + "edgemanifest": "Edge Manifest" } }, "examples": { diff --git a/src/components/pages/packages/flyouts/deletePackage/deletePackage.container.js b/src/components/pages/packages/flyouts/deletePackage/deletePackage.container.js new file mode 100644 index 000000000..9ddeb9882 --- /dev/null +++ b/src/components/pages/packages/flyouts/deletePackage/deletePackage.container.js @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { DeletePackage } from './deletePackage'; +import { + getDeletePackageError, + getDeletePackagePendingStatus, + epics as packagesEpics +} from 'store/reducers/packagesReducer'; + +// Pass the global info needed +const mapStateToProps = state => ({ + isPending: getDeletePackagePendingStatus(state), + error: getDeletePackageError(state) +}); + +// Wrap the dispatch methods +const mapDispatchToProps = dispatch => ({ + deletePackage: packageId => dispatch(packagesEpics.actions.deletePackage(packageId)) +}); + +export const DeletePackageContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeletePackage)); diff --git a/src/components/pages/packages/flyouts/deletePackage/deletePackage.js b/src/components/pages/packages/flyouts/deletePackage/deletePackage.js new file mode 100644 index 000000000..e13b3d0e5 --- /dev/null +++ b/src/components/pages/packages/flyouts/deletePackage/deletePackage.js @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; + +import { LinkedComponent } from 'utilities'; +import { + AjaxError, + Btn, + BtnToolbar, + Indicator, + Modal +} from 'components/shared'; +import { svgs } from 'utilities'; + +import './deletePackage.css'; + +export class DeletePackage extends LinkedComponent { + + constructor(props) { + super(props); + + this.state = { + changesApplied: false + }; + } + + componentWillReceiveProps({ error, isPending, onClose }) { + if (this.state.changesApplied && !error && !isPending) { + onClose(); + } + } + + apply = () => { + const { deletePackage, package: { id } } = this.props; + deletePackage(id); + this.setState({ changesApplied: true }); + } + + render() { + const { t, onClose, isPending, error } = this.props; + const { changesApplied } = this.state; + + return ( + +
+
{t('packages.flyouts.delete.title')}
+ +
+
+ {t('packages.flyouts.delete.info')} +
+
+ { + !changesApplied && + {t('packages.flyouts.delete.delete')} + {t('packages.flyouts.delete.cancel')} + + } + {isPending && } + {changesApplied && error && } +
+
+ ); + } +} diff --git a/src/components/pages/packages/flyouts/deletePackage/deletePackage.scss b/src/components/pages/packages/flyouts/deletePackage/deletePackage.scss new file mode 100644 index 000000000..91da7db2a --- /dev/null +++ b/src/components/pages/packages/flyouts/deletePackage/deletePackage.scss @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/variables'; +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.delete-package-container { + @include rem-fallback(font-size, 14px); + + .delete-header-container { + display: grid; + grid-template-columns: auto auto; + justify-content: space-between; + @include rem-fallback(padding-bottom, 24px); + } + + .delete-info { + @include rem-fallback(padding-bottom, 24px); + @include rem-fallback(height, 60px); + } + + .delete-close-btn, + .delete-close-btn:hover { + background-color: transparent; + @include rem-fallback(padding, 0px, 16px); + @include rem-fallback(min-width, 14px); + + &:hover { background-color: transparent; } + + .btn-icon svg { @include square-px-rem(14px); } + } + + @include themify($themes) { + .delete-close-btn .btn-icon svg { stroke: themed('colorFlyoutText'); } + } +} diff --git a/src/components/pages/packages/flyouts/deletePackage/index.js b/src/components/pages/packages/flyouts/deletePackage/index.js new file mode 100644 index 000000000..90a352d87 --- /dev/null +++ b/src/components/pages/packages/flyouts/deletePackage/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './deletePackage'; +export * from './deletePackage.container'; diff --git a/src/components/pages/packages/flyouts/index.js b/src/components/pages/packages/flyouts/index.js index 485cef754..1602ebc7b 100644 --- a/src/components/pages/packages/flyouts/index.js +++ b/src/components/pages/packages/flyouts/index.js @@ -1,3 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. export * from './newPackage'; +export * from './deletePackage'; diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.js b/src/components/pages/packages/flyouts/newPackage/newPackage.js index a7c89185e..3cd54d8d7 100644 --- a/src/components/pages/packages/flyouts/newPackage/newPackage.js +++ b/src/components/pages/packages/flyouts/newPackage/newPackage.js @@ -123,7 +123,7 @@ export class NewPackage extends LinkedComponent { {packageFile &&
{packageFile.name}
} { completedSuccessfully && -
+
{t('packages.flyouts.new.deploymentText')}
} diff --git a/src/components/pages/packages/packagesGrid/packagesGrid.js b/src/components/pages/packages/packagesGrid/packagesGrid.js index c7d1b5dd6..56277ae66 100644 --- a/src/components/pages/packages/packagesGrid/packagesGrid.js +++ b/src/components/pages/packages/packagesGrid/packagesGrid.js @@ -5,6 +5,7 @@ import { packagesColumnDefs, defaultPackagesGridProps } from './packagesGridConf import { Btn, PcsGrid, Protected } from 'components/shared'; import { isFunc, translateColumnDefs, svgs } from 'utilities'; import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig'; +import { DeletePackageContainer } from '../flyouts'; import './packagesGrid.css'; @@ -18,7 +19,8 @@ export class PackagesGrid extends Component { // Set the initial state this.state = { - ...closedFlyoutState + ...closedFlyoutState, + hardSelectedPackages: [] }; this.columnDefs = [ @@ -30,11 +32,18 @@ export class PackagesGrid extends Component { this.contextBtns = [ - {props.t('packages.delete')} + {props.t('packages.delete')} ]; } + getOpenFlyout = () => { + if (this.state.openFlyoutName === 'delete-package') { + return + } + return null; + } + /** * Get the grid api options * @@ -56,7 +65,10 @@ export class PackagesGrid extends Component { onHardSelectChange = (selectedPackages) => { const { onContextMenuChange, onHardSelectChange } = this.props; if (isFunc(onContextMenuChange)) { - onContextMenuChange(selectedPackages.length > 0 ? this.contextBtns : null); + onContextMenuChange(selectedPackages.length === 1 ? this.contextBtns : null); + this.setState({ + hardSelectedPackages: selectedPackages + }); } if (isFunc(onHardSelectChange)) { onHardSelectChange(selectedPackages); @@ -87,6 +99,9 @@ export class PackagesGrid extends Component { } }; - return (); + return ([ + , + this.getOpenFlyout() + ]); } } diff --git a/src/components/shared/index.js b/src/components/shared/index.js index 1b8ac07ae..b39a66156 100644 --- a/src/components/shared/index.js +++ b/src/components/shared/index.js @@ -15,3 +15,4 @@ export * from './propertyGrid' export * from './protected' export * from './refreshBar/refreshBar'; export * from './svg/svg'; +export * from './modal'; diff --git a/src/components/shared/modal/README.md b/src/components/shared/modal/README.md new file mode 100644 index 000000000..e687d437f --- /dev/null +++ b/src/components/shared/modal/README.md @@ -0,0 +1,24 @@ +Modal Components +================================= + +These are presentational components for creating modals. + +### ModalFadeBox:  + +A presentational component which is styled to fade the rest of the screen at 0.5 opacity. +  +### ModalContainer:  + +A presentational component which contains the contents of the modal. + +### Modal:  + +A presentational component which contains ModalFadeBox and ModalContainer. + +## Example:  + +```jsx + + Sample Content + +``` diff --git a/src/components/shared/modal/index.js b/src/components/shared/modal/index.js new file mode 100644 index 000000000..8edfeb8de --- /dev/null +++ b/src/components/shared/modal/index.js @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './modalFadeBox/modalFadeBox'; +export * from './modalContent/modalContent'; +export * from './modal/modal'; diff --git a/src/components/shared/modal/modal/modal.js b/src/components/shared/modal/modal/modal.js new file mode 100644 index 000000000..8f82aae49 --- /dev/null +++ b/src/components/shared/modal/modal/modal.js @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; + +import { + ModalContent, + ModalFadeBox +} from '..'; +import { joinClasses } from 'utilities'; + +import './modal.css'; + +export class Modal extends Component { + + componentDidMount() { + if (this.props.onClose) { + window.addEventListener('keydown', this.listenKeyboard, true); + } + } + + componentWillUnmount() { + if (this.props.onClose) { + window.removeEventListener('keydown', this.listenKeyboard, true); + } + } + + listenKeyboard = (event) => { + if (event.key === 'Escape' || event.keyCode === 27) { + this.props.onClose(); + } + } + + onOverlayClick = () => { + this.props.onClose(); + } + + render() { + return ( +
+ + + {this.props.children} + +
+ ); + } +} diff --git a/src/components/shared/modal/modal/modal.scss b/src/components/shared/modal/modal/modal.scss new file mode 100644 index 000000000..c2a58e0a4 --- /dev/null +++ b/src/components/shared/modal/modal/modal.scss @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/mixins'; + +.modal-container { + width: 100%; + height: 100%; + position: fixed; + display: flex; + align-items: center; + justify-content: center; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index:1000; +} diff --git a/src/components/shared/modal/modalContent/modalContent.js b/src/components/shared/modal/modalContent/modalContent.js new file mode 100644 index 000000000..7a229d13d --- /dev/null +++ b/src/components/shared/modal/modalContent/modalContent.js @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; + +import { joinClasses } from 'utilities'; + +import './modalContent.css'; + +/** A presentational component containing the content of the modal */ +export const ModalContent = ({ children, className }) => ( +
{children}
+); diff --git a/src/components/shared/modal/modalContent/modalContent.scss b/src/components/shared/modal/modalContent/modalContent.scss new file mode 100644 index 000000000..2f6a715f7 --- /dev/null +++ b/src/components/shared/modal/modalContent/modalContent.scss @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/mixins'; +@import 'src/styles/themes'; +.modal-container { + .modal-content { + border-style: solid; + width: 400px; + height: 200px; + z-index: 1; + @include rem-fallback(border-width, 1px); + @include rem-fallback(padding, 20px); + + @include themify($themes) { + color: themed('colorModalText'); + background-color: themed('colorModalBackground'); + border-color: themed('colorModalBorder'); + } + } +} diff --git a/src/components/shared/modal/modalFadeBox/modalFadeBox.js b/src/components/shared/modal/modalFadeBox/modalFadeBox.js new file mode 100644 index 000000000..ead16071d --- /dev/null +++ b/src/components/shared/modal/modalFadeBox/modalFadeBox.js @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; + +import { joinClasses } from 'utilities'; + +import './modalFadeBox.css'; + +/** A presentational component containing the content of the modal */ +export const ModalFadeBox = ({ children, className, onClick }) => ( +
{children}
+); diff --git a/src/components/shared/modal/modalFadeBox/modalFadeBox.scss b/src/components/shared/modal/modalFadeBox/modalFadeBox.scss new file mode 100644 index 000000000..ec18d7ed4 --- /dev/null +++ b/src/components/shared/modal/modalFadeBox/modalFadeBox.scss @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.modal-container { + .modal-fade-box { + position: absolute; + width: 100%; + height: 100%; + opacity: 0.5; + z-index: 0; + + @include themify($themes) { + background-color: themed('colorModalDropShadow'); + } + } +} diff --git a/src/services/models/configModels.js b/src/services/models/configModels.js index b5a6580f3..26dcaf14c 100644 --- a/src/services/models/configModels.js +++ b/src/services/models/configModels.js @@ -54,7 +54,7 @@ export const toSolutionSettingThemeModel = (response = {}) => camelCaseReshape(r 'azureMapsKey': 'azureMapsKey' }); -export const packageTypeOptions = ['EDGE_MANIFEST']; +export const packageTypeOptions = ['EdgeManifest']; export const toNewPackageRequestModel = ({ type, diff --git a/src/store/reducers/packagesReducer.js b/src/store/reducers/packagesReducer.js index e2ad48fdf..50249d803 100644 --- a/src/store/reducers/packagesReducer.js +++ b/src/store/reducers/packagesReducer.js @@ -75,17 +75,11 @@ const insertPackageReducer = (state, { payload, fromAction }) => { }); }; -const deletePackagesReducer = (state, { payload, fromAction }) => { - const spliceArr = payload.reduce((idxAcc, payloadItem) => { - const idx = state.items.indexOf(payloadItem); - if (idx !== -1) { - idxAcc.push([idx, 1]); - } - return idxAcc; - }, []); +const deletePackageReducer = (state, { payload, fromAction }) => { + const idx = state.items.indexOf(payload); return update(state, { - entities: { $unset: payload }, - items: { $splice: spliceArr }, + entities: { $unset: [payload] }, + items: { $splice: [[idx, 1]] }, ...setPending(fromAction.type, false) }); }; @@ -109,7 +103,7 @@ const fetchableTypes = [ export const redux = createReducerScenario({ insertPackage: { type: 'PACKAGE_INSERT', reducer: insertPackageReducer }, - deletePackages: { type: 'PACKAGES_DELETE', reducer: deletePackagesReducer }, + deletePackage: { type: 'PACKAGES_DELETE', reducer: deletePackageReducer }, updatePackages: { type: 'PACKAGES_UPDATE', reducer: updatePackagesReducer }, registerError: { type: 'PACKAGES_REDUCER_ERROR', reducer: errorReducer }, isFetching: { multiType: fetchableTypes, reducer: pendingReducer } diff --git a/src/styles/_themes.scss b/src/styles/_themes.scss index 20b9d12d2..4ff8f2963 100644 --- a/src/styles/_themes.scss +++ b/src/styles/_themes.scss @@ -156,6 +156,13 @@ $themes: ( colorFormActionsBorderColor: $colorGraphite, colorDurationLabelText: $colorSmoke, // Forms - END + + // Modal - START + colorModalText: $colorWhite, + colorModalBackground: $colorNoir, + colorModalBorder: #60AAFF, + colorModalDropShadow: rgba(0, 0, 0, 0.6), + // Modal - END ), light: ( // Functional colors @@ -308,5 +315,12 @@ $themes: ( colorFormActionsBorderColor: $colorGraphite, colorDurationLabelText: $colorSmoke, // Forms - END + + // Modal - START + colorModalText: #333, + colorModalBackground: $colorWhite, + colorModalBorder: #136BFB, + colorModalDropShadow: rgba(0, 0, 0, 0.1), + // Modal - END ) ); From 6c80712259227350962eb58b39a70a8269102eff Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 31 Aug 2018 17:27:42 -0700 Subject: [PATCH 06/25] Merge Master into Edge feature branch (#1083) * Dev Walkthru: add a new Panel to the Dashboard (#1062) * Dev Walkthru: add a new Panel to the Dashboard * small tweaks, review feedback * fix bad code end marker * Diagnostics bugFix (#1065) * flatMap * Dummy comment to retrigger build * Add Rule Diagnostics (#1064) Add diagnostics logging for rule create/update events. Added the following metrics: Rule_NewClick Rule_EditClick Rule_DeviceGroupClick Rule_CalculationClick Rule_FieldClick Rule_OperatorClick Rule_AddConditionClick Rule_SeverityLevelClick Rule_StatusToggle Rule_ApplyClick Rule_CancelClick Rule_TopXCloseClick Also includes new "sessionid" sections of diagnostics call, which logs the time in ms since Jan 1, 1970 when the page was loaded (amplitude expects session id in this format). This fields will be added by diagnostics to enable logging of session id to amplitude--until those changes go in it will be ignored by the backend. * Delete .travis.yml (#1066) * Delete .travis.yml * Update README.md * Add diagnostics for new device funnel (#1075) * Add device metrics Add metrics for new device flyout * Add device created metric Add metric on device create so we can log device id for physical devices * Fix rule apply event Rule apply click event was only emitted for new rules. Move call so it is emitted if a rule is added or edited. * Address comments and align metric names Address comments. Update metric names to be in same format as rule metrics * fix insertion when entities are null for devices and rules (#1078) * Refactor to make walkthrough code less intrusive (#1069) * refactor to make walkthrough code less intrusive * refactor after meeting with team * update MD files, move httpClient * review feedback * fix nit * update breadcrumbs to use isDef instead of checking undefined * example * add packages --- .travis.yml | 17 --- README.md | 4 +- docs/walkthrough/addNewDashboardPanel.md | 12 +- docs/walkthrough/addNewFlyout.md | 16 +-- docs/walkthrough/addNewGrid.md | 18 +-- docs/walkthrough/addNewPage.md | 51 ++++--- docs/walkthrough/addNewService.md | 20 +-- public/locales/en/translations.json | 39 +++-- src/app.config.js | 6 +- src/components/{app => }/app.container.js | 4 +- src/components/app.js | 134 ++++++++++++++++++ src/components/app/app.js | 113 --------------- src/components/app/header/breadcrumbs.js | 36 ----- .../pages/_example/example.container.js | 7 - .../_flyoutExample/flyoutExample.container.js | 7 - src/components/pages/dashboard/dashboard.js | 10 +- .../pages/devices/devices.container.js | 4 +- src/components/pages/devices/devices.js | 11 +- .../flyouts/deviceNew/deviceNew.container.js | 6 +- .../devices/flyouts/deviceNew/deviceNew.js | 40 +++++- src/components/pages/index.js | 6 - .../maintenance/jobDetails/jobDetails.js | 2 +- .../maintenance/ruleDetails/ruleDetails.js | 6 +- .../pages/maintenance/summary/summary.js | 6 +- .../rules/flyouts/ruleEditor/ruleEditor.js | 2 +- src/components/pages/rules/rules.js | 4 +- .../deviceGroupDropdown.container.js | 0 .../deviceGroupDropdown.js | 0 .../deviceGroupDropdown.scss | 0 .../deviceGroupDropdown/index.js | 0 .../{app => shell}/flyouts/index.js | 0 .../flyouts/manageDeviceGroups/index.js | 0 .../manageDeviceGroups.container.js | 0 .../manageDeviceGroups/manageDeviceGroups.js | 0 .../manageDeviceGroups.scss | 0 .../views/deviceGroupForm.js | 0 .../manageDeviceGroups/views/deviceGroups.js | 0 .../flyouts/settings/applicationSettings.js | 0 .../flyouts/settings/applicationSettings.scss | 0 .../{app => shell}/flyouts/settings/index.js | 0 .../flyouts/settings/settings.container.js | 0 .../flyouts/settings/settings.js | 2 +- .../flyouts/settings/settings.scss | 0 src/components/shell/header/breadcrumbs.js | 37 +++++ .../{app => shell}/header/header.js | 2 +- .../{app => shell}/header/header.scss | 0 src/components/{app => shell}/main/main.js | 0 src/components/{app => shell}/main/main.scss | 0 .../manageDeviceGroupsBtn/index.js | 0 .../manageDeviceGroupsBtn.container.js | 0 .../manageDeviceGroupsBtn.js | 0 .../{app => shell}/navigation/navigation.js | 0 .../{app => shell}/navigation/navigation.scss | 0 .../navigation/navigationContainer.js | 0 src/components/shell/pageNotFound/index.js | 4 + .../pageNotFound/pageNotFound.container.js | 0 .../pageNotFound/pageNotFound.js | 0 .../pageNotFound/pageNotFound.scss | 0 src/components/shell/shell.container.js | 25 ++++ src/components/shell/shell.js | 57 ++++++++ .../{app/app.scss => shell/shell.scss} | 4 +- .../{app/app.test.js => shell/shell.test.js} | 8 +- .../timeIntervalDropdown/index.js | 0 .../timeIntervalDropdown.js | 0 .../timeIntervalDropdown.scss | 0 src/index.js | 16 ++- src/services/authService.js | 2 +- src/services/configService.js | 2 +- src/services/deviceSimulationService.js | 2 +- src/services/diagnosticsService.js | 2 +- src/services/gitHubService.js | 2 +- src/services/index.js | 2 - src/services/iotHubManagerService.js | 2 +- src/services/models/index.js | 2 - src/services/models/logEventModels.js | 14 ++ src/services/telemetryService.js | 2 +- src/store/configureStore.js | 2 +- src/store/reducers/appReducer.js | 11 ++ src/store/reducers/devicesReducer.js | 10 +- src/store/reducers/rulesReducer.js | 10 +- src/store/rootEpic.js | 2 - src/store/rootReducer.js | 2 - .../models => utilities}/ajaxModels.js | 0 src/{services => utilities}/httpClient.js | 4 +- .../httpClient.test.js | 2 +- src/utilities/index.js | 2 + src/walkthrough/components/app.container.js | 25 ++++ src/walkthrough/components/app.js | 80 +++++++++++ .../pages/basicPage/basicPage.container.js | 7 + .../components/pages/basicPage/basicPage.js} | 8 +- .../pages/basicPage/basicPage.scss} | 2 +- .../pages/basicPage/basicPage.test.js} | 6 +- .../pages/dashboard/dashboard.container.js | 8 ++ .../components/pages/dashboard/dashboard.js | 56 ++++++++ .../components/pages/dashboard/dashboard.scss | 31 ++++ .../pages/dashboard/dashboard.test.js | 20 +++ .../panels/examplePanel/examplePanel.js | 35 +++++ .../panels/examplePanel/examplePanel.scss | 14 ++ .../dashboard/panels/examplePanel/index.js | 3 + .../pages/dashboard/panels/index.js | 3 + src/walkthrough/components/pages/index.js | 8 ++ .../exampleFlyout/exampleFlyout.container.js | 0 .../flyouts/exampleFlyout/exampleFlyout.js | 24 ++-- .../flyouts/exampleFlyout/exampleFlyout.scss | 0 .../flyouts/exampleFlyout/index.js | 0 .../pageWithFlyout.container.js | 7 + .../pages/pageWithFlyout/pageWithFlyout.js} | 10 +- .../pages/pageWithFlyout/pageWithFlyout.scss} | 2 +- .../pageWithFlyout/pageWithFlyout.test.js} | 6 +- .../pageWithGrid}/exampleGrid/exampleGrid.js | 4 +- .../exampleGrid/exampleGridConfig.js | 4 +- .../pages/pageWithGrid}/exampleGrid/index.js | 0 .../pageWithGrid/pageWithGrid.container.js} | 6 +- .../pages/pageWithGrid/pageWithGrid.js} | 6 +- .../pages/pageWithGrid/pageWithGrid.scss} | 2 +- .../pages/pageWithGrid/pageWithGrid.test.js} | 6 +- .../services/exampleService.js} | 0 src/walkthrough/services/index.js | 5 + .../services/models/exampleModels.js} | 0 src/walkthrough/services/models/index.js | 5 + src/walkthrough/store/configureStore.js | 19 +++ .../store/reducers/exampleReducer.js} | 2 +- src/walkthrough/store/rootEpic.js | 19 +++ src/walkthrough/store/rootReducer.js | 16 +++ 124 files changed, 900 insertions(+), 362 deletions(-) delete mode 100644 .travis.yml rename src/components/{app => }/app.container.js (84%) create mode 100644 src/components/app.js delete mode 100644 src/components/app/app.js delete mode 100644 src/components/app/header/breadcrumbs.js delete mode 100644 src/components/pages/_example/example.container.js delete mode 100644 src/components/pages/_flyoutExample/flyoutExample.container.js rename src/components/{app => shell}/deviceGroupDropdown/deviceGroupDropdown.container.js (100%) rename src/components/{app => shell}/deviceGroupDropdown/deviceGroupDropdown.js (100%) rename src/components/{app => shell}/deviceGroupDropdown/deviceGroupDropdown.scss (100%) rename src/components/{app => shell}/deviceGroupDropdown/index.js (100%) rename src/components/{app => shell}/flyouts/index.js (100%) rename src/components/{app => shell}/flyouts/manageDeviceGroups/index.js (100%) rename src/components/{app => shell}/flyouts/manageDeviceGroups/manageDeviceGroups.container.js (100%) rename src/components/{app => shell}/flyouts/manageDeviceGroups/manageDeviceGroups.js (100%) rename src/components/{app => shell}/flyouts/manageDeviceGroups/manageDeviceGroups.scss (100%) rename src/components/{app => shell}/flyouts/manageDeviceGroups/views/deviceGroupForm.js (100%) rename src/components/{app => shell}/flyouts/manageDeviceGroups/views/deviceGroups.js (100%) rename src/components/{app => shell}/flyouts/settings/applicationSettings.js (100%) rename src/components/{app => shell}/flyouts/settings/applicationSettings.scss (100%) rename src/components/{app => shell}/flyouts/settings/index.js (100%) rename src/components/{app => shell}/flyouts/settings/settings.container.js (100%) rename src/components/{app => shell}/flyouts/settings/settings.js (99%) rename src/components/{app => shell}/flyouts/settings/settings.scss (100%) create mode 100644 src/components/shell/header/breadcrumbs.js rename src/components/{app => shell}/header/header.js (97%) rename src/components/{app => shell}/header/header.scss (100%) rename src/components/{app => shell}/main/main.js (100%) rename src/components/{app => shell}/main/main.scss (100%) rename src/components/{app => shell}/manageDeviceGroupsBtn/index.js (100%) rename src/components/{app => shell}/manageDeviceGroupsBtn/manageDeviceGroupsBtn.container.js (100%) rename src/components/{app => shell}/manageDeviceGroupsBtn/manageDeviceGroupsBtn.js (100%) rename src/components/{app => shell}/navigation/navigation.js (100%) rename src/components/{app => shell}/navigation/navigation.scss (100%) rename src/components/{app => shell}/navigation/navigationContainer.js (100%) create mode 100644 src/components/shell/pageNotFound/index.js rename src/components/{pages => shell}/pageNotFound/pageNotFound.container.js (100%) rename src/components/{pages => shell}/pageNotFound/pageNotFound.js (100%) rename src/components/{pages => shell}/pageNotFound/pageNotFound.scss (100%) create mode 100644 src/components/shell/shell.container.js create mode 100644 src/components/shell/shell.js rename src/components/{app/app.scss => shell/shell.scss} (96%) rename src/components/{app/app.test.js => shell/shell.test.js} (60%) rename src/components/{app => shell}/timeIntervalDropdown/index.js (100%) rename src/components/{app => shell}/timeIntervalDropdown/timeIntervalDropdown.js (100%) rename src/components/{app => shell}/timeIntervalDropdown/timeIntervalDropdown.scss (100%) rename src/{services/models => utilities}/ajaxModels.js (100%) rename src/{services => utilities}/httpClient.js (97%) rename src/{services => utilities}/httpClient.test.js (89%) create mode 100644 src/walkthrough/components/app.container.js create mode 100644 src/walkthrough/components/app.js create mode 100644 src/walkthrough/components/pages/basicPage/basicPage.container.js rename src/{components/pages/_example/example.js => walkthrough/components/pages/basicPage/basicPage.js} (56%) rename src/{components/pages/_example/example.scss => walkthrough/components/pages/basicPage/basicPage.scss} (71%) rename src/{components/pages/_example/example.test.js => walkthrough/components/pages/basicPage/basicPage.test.js} (70%) create mode 100644 src/walkthrough/components/pages/dashboard/dashboard.container.js create mode 100644 src/walkthrough/components/pages/dashboard/dashboard.js create mode 100644 src/walkthrough/components/pages/dashboard/dashboard.scss create mode 100644 src/walkthrough/components/pages/dashboard/dashboard.test.js create mode 100644 src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.js create mode 100644 src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.scss create mode 100644 src/walkthrough/components/pages/dashboard/panels/examplePanel/index.js create mode 100644 src/walkthrough/components/pages/dashboard/panels/index.js create mode 100644 src/walkthrough/components/pages/index.js rename src/{components/pages/_flyoutExample => walkthrough/components/pages/pageWithFlyout}/flyouts/exampleFlyout/exampleFlyout.container.js (100%) rename src/{components/pages/_flyoutExample => walkthrough/components/pages/pageWithFlyout}/flyouts/exampleFlyout/exampleFlyout.js (76%) rename src/{components/pages/_flyoutExample => walkthrough/components/pages/pageWithFlyout}/flyouts/exampleFlyout/exampleFlyout.scss (100%) rename src/{components/pages/_flyoutExample => walkthrough/components/pages/pageWithFlyout}/flyouts/exampleFlyout/index.js (100%) create mode 100644 src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.container.js rename src/{components/pages/_flyoutExample/flyoutExample.js => walkthrough/components/pages/pageWithFlyout/pageWithFlyout.js} (78%) rename src/{components/pages/_flyoutExample/flyoutExample.scss => walkthrough/components/pages/pageWithFlyout/pageWithFlyout.scss} (69%) rename src/{components/pages/_flyoutExample/flyoutExample.test.js => walkthrough/components/pages/pageWithFlyout/pageWithFlyout.test.js} (66%) rename src/{components/pages/_gridExample => walkthrough/components/pages/pageWithGrid}/exampleGrid/exampleGrid.js (96%) rename src/{components/pages/_gridExample => walkthrough/components/pages/pageWithGrid}/exampleGrid/exampleGridConfig.js (86%) rename src/{components/pages/_gridExample => walkthrough/components/pages/pageWithGrid}/exampleGrid/index.js (100%) rename src/{components/pages/_gridExample/gridExample.container.js => walkthrough/components/pages/pageWithGrid/pageWithGrid.container.js} (74%) rename src/{components/pages/_gridExample/gridExample.js => walkthrough/components/pages/pageWithGrid/pageWithGrid.js} (89%) rename src/{components/pages/_gridExample/gridExample.scss => walkthrough/components/pages/pageWithGrid/pageWithGrid.scss} (70%) rename src/{components/pages/_gridExample/gridExample.test.js => walkthrough/components/pages/pageWithGrid/pageWithGrid.test.js} (75%) rename src/{services/_exampleService.js => walkthrough/services/exampleService.js} (100%) create mode 100644 src/walkthrough/services/index.js rename src/{services/models/_exampleModels.js => walkthrough/services/models/exampleModels.js} (100%) create mode 100644 src/walkthrough/services/models/index.js create mode 100644 src/walkthrough/store/configureStore.js rename src/{store/reducers/_exampleReducer.js => walkthrough/store/reducers/exampleReducer.js} (98%) create mode 100644 src/walkthrough/store/rootEpic.js create mode 100644 src/walkthrough/store/rootReducer.js diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3e3f73d0f..000000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -cache: - directories: - - node_modules -sudo: false -language: node_js -node_js: -- '6' -before_install: -- npm install -script: -- npm run build-css -- npm test -- npm run build -notifications: - slack: - rooms: - - secure: hLmNwol2KZ9D/2tvpVH6cpa0Ri07XthTc+92gyAIUnHKi8bOlLSuwUV28JumHxwMqcOeas5K1lq16nLt6K8eZzcwHVuUZRTPIwKJm96bnwS/z3TeTun2egnWxfnXotj+Bjrxjid9UtmZ9EnGUd1MfavVOn4q3O7u2kQHmBKbTSMOimmrjWcVjA1XBNNrj4Ec8I/9NZFmLmCzkzA2oyjEMfZ/RLvFSJz6JAdpomjMScIJ0W3abanZwXl6pN7ertfv6YSoPvmBKzjr28pdepDKHfRrQyN66OE+mLhVA2xFXrTAIVdeghN6F7U1XBMMRqvmgb2V8i449FVQc9e9Sjem7fYyOvZgg392Nef6xZ02zS/pNGZhXQbpnFrJ6HAl5ueFJGYaTAz0WGX+RQzzuTF78QIdg1Ye0sOXNpZeF+mnclQJvMVawq34F7NAWK0u+DGxLyhtGEx87wS/wVScseMD7dmBYHkBHGkznGJl+s+Uwsg0s+uK6beL9QgW3xifWbOnUzCif8BiPEU3xBUKpTdBFstSS96Ju1t+bltR6VGn/7OpQpoio1vwFmCdYxxlKrLGLP6A0Ybi7rkbVkXnSAjY6vW6/MJoE4W16otZiT65CB0qUH1TEhnD2+XhNkTnHbddRfzdqzd8T2DZNqk5V3IrtxnHjHtMbuLvvcdOYHKSAUc= \ No newline at end of file diff --git a/README.md b/README.md index 241a40706..70737ecee 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,8 @@ You can find a guide to using it [here](https://github.com/facebookincubator/cre - [SASS](http://sass-lang.com/) - [React-i18nnext](https://github.com/i18next/react-i18next) -[build-badge]: https://img.shields.io/travis/Azure/pcs-remote-monitoring-webui.svg -[build-url]: https://travis-ci.org/Azure/pcs-remote-monitoring-webui +[build-badge]: https://solutionaccelerators.visualstudio.com/RemoteMonitoring/_apis/build/status/pcs-remote-monitoring-webui +[build-url]: https://solutionaccelerators.visualstudio.com/RemoteMonitoring/_build/latest?definitionId=32 [issues-badge]: https://img.shields.io/github/issues/azure/pcs-remote-monitoring-webui.svg [issues-url]: https://github.com/Azure/pcs-remote-monitoring-webui/issues/new [gitter-badge]: https://img.shields.io/gitter/room/azure/iot-solutions.js.svg diff --git a/docs/walkthrough/addNewDashboardPanel.md b/docs/walkthrough/addNewDashboardPanel.md index 01ddd0111..92db46777 100644 --- a/docs/walkthrough/addNewDashboardPanel.md +++ b/docs/walkthrough/addNewDashboardPanel.md @@ -5,10 +5,10 @@ The following is for creating a new panel called "**examplePanel**." 1. Create a folder named `examplePanel` inside the `components/pages/dashboard/panels` folder. 1. Create 3 files in the new folder. See the individual example files for more details and comments inline. - - [examplePanel.js](/src/components/pages/dashboard/panels/_examplePanel/examplePanel.js) - main component for the panel - - [examplePanel.scss](/src/components/pages/dashboard/panels/_examplePanel/examplePanel.scss) - styles for the new panel - - [index.js](/src/components/pages/dashboard/panels/_examplePanel/index.js) - exports for the new panel -1. Add the new panel to the main panel export file: [dashboard/panels/index.js](/src/components/pages/dashboard/panels/index.js). + - [examplePanel.js](/src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.js) - main component for the panel + - [examplePanel.scss](/src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.scss) - styles for the new panel + - [index.js](/src/walkthrough/components/pages/dashboard/panels/examplePanel/index.js) - exports for the new panel +1. Add the new panel to the main panel export file: [dashboard/panels/index.js](/src/walkthrough/components/pages/dashboard/panels/index.js). ```js export * from './examplePanel'; ``` @@ -18,7 +18,7 @@ The following is for creating a new panel called "**examplePanel**." "header": "Example Panel", }, ``` -1. In the [examplePanel.js](/src/components/pages/dashboard/panels/_examplePanel/examplePanel.js), import the `Panel` components. +1. In the [examplePanel.js](/src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.js), import the `Panel` components. ```js import { Panel, @@ -38,7 +38,7 @@ The following is for creating a new panel called "**examplePanel**." ``` -1. Add your panel to the [dashboard.js](/src/components/pages/dashboard/dashboard.js) page. Size the `Cell` for the panel according to how much space it will need. See [grid.scss](/src/components/pages/dashboard/grid/grid.scss) for the available grid-cell styles. +1. Add your panel to the [dashboard.js](/src/walkthrough/components/pages/dashboard/dashboard.js) page. Size the `Cell` for the panel according to how much space it will need. See [grid.scss](/src/components/pages/dashboard/grid/grid.scss) for the available grid-cell styles. ```jsx diff --git a/docs/walkthrough/addNewFlyout.md b/docs/walkthrough/addNewFlyout.md index a66ffa1c5..d217f4d6b 100644 --- a/docs/walkthrough/addNewFlyout.md +++ b/docs/walkthrough/addNewFlyout.md @@ -11,16 +11,16 @@ The following is for creating a new flyout called "**exampleFlyout**." ### Create the new flyout 1. Create a folder named `exampleFlyout` inside your page's `flyouts` folder. 1. Create 4 files in the new folder. See the individual example files for more details and comments inline. - - [exampleFlyout.container.js](/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.container.js) - maps redux and epic actions and selectors to the props for the flyout - - [exampleFlyout.js](/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.js) - main component for the flyout - - [exampleFlyout.scss](/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.scss) - styles for the flyout - - [index.js](/src/components/pages/_flyoutExample/flyouts/exampleFlyout/index.js) - exports for the new flyout + - [exampleFlyout.container.js](/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.container.js) - maps redux and epic actions and selectors to the props for the flyout + - [exampleFlyout.js](/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.js) - main component for the flyout + - [exampleFlyout.scss](/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.scss) - styles for the flyout + - [index.js](/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/index.js) - exports for the new flyout ### Setup the flyout -1. Open your flyout's container file [exampleFlyout.container.js](/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.container.js) so the data and actions can be connected to the page props. +1. Open your flyout's container file [exampleFlyout.container.js](/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.container.js) so the data and actions can be connected to the page props. - To keep our example simple, no actions are mapped. But in a real world scenario, you would very likely need this. See the [Add a New Grid walkthrough](addNewGrid.md) for more information on mapping data and actions via a `container.js.` -1. Open your flyout's file [exampleFlyout.js](/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.js). Use the various Flyout components to ensure consistency with others. Then, add whatever components are needed inside `FlyoutContent`. Notice that the FlyoutCloseBtn provides a way to close the flyout. +1. Open your flyout's file [exampleFlyout.js](/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.js). Use the various Flyout components to ensure consistency with others. Then, add whatever components are needed inside `FlyoutContent`. Notice that the FlyoutCloseBtn provides a way to close the flyout. ```jsx @@ -38,7 +38,7 @@ The following is for creating a new flyout called "**exampleFlyout**." ``` ### Open the flyout from a page -1. Open your page's file [flyoutExample.js](/src/components/pages/_flyoutExample/flyoutExample.js). +1. Open your page's file [pageWithFlyout.js](/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.js). 1. In the render method, add a context button to open the flyout. ```jsx @@ -66,7 +66,7 @@ The following is for creating a new flyout called "**exampleFlyout**." ## More Advanced Topics ### Do some action and show progress -Often, a flyout will be used to call a service to create/update/delete something. See the [exampleFlyout.js](/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.js) file for an example of +Often, a flyout will be used to call a service to create/update/delete something. See the [exampleFlyout.js](/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.js) file for an example of - using `SummarySection` to indicate how many items will be affected - showing an `Indicator` (progress spinner) while an action is in progress - showing an `Svg` checkmark when the action is complete diff --git a/docs/walkthrough/addNewGrid.md b/docs/walkthrough/addNewGrid.md index c153a4108..ebe4c4df2 100644 --- a/docs/walkthrough/addNewGrid.md +++ b/docs/walkthrough/addNewGrid.md @@ -15,12 +15,12 @@ Grids in remote monitoring are based on [ag-grid][ag-grid], with our own customi ### Create the new grid 1. Create a folder named `exampleGrid` inside your page's folder. 1. Create 3 files in the new folder. See the individual example files for more details and comments inline. - - [exampleGrid.js](/src/components/pages/_gridExample/exampleGrid/exampleGrid.js) - main component for the grid, sets up context buttons and soft/hard selection event handlers, wraps [pcsGrid][pcsGrid] - - [exampleGridConfig.js](/src/components/pages/_gridExample/exampleGrid/exampleGridConfig.js) - configuration such as column definitions for the grid - - [index.js](/src/components/pages/_gridExample/exampleGrid/index.js) - exports for the new grid + - [exampleGrid.js](/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js) - main component for the grid, sets up context buttons and soft/hard selection event handlers, wraps [pcsGrid][pcsGrid] + - [exampleGridConfig.js](/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGridConfig.js) - configuration such as column definitions for the grid + - [index.js](/src/walkthrough/components/pages/pageWithGrid/exampleGrid/index.js) - exports for the new grid ### Setup the page -1. Open your page's container file [gridExample.container.js](/src/components/pages/_gridExample/gridExample.container.js) so the data and actions can be connected to the page props. +1. Open your page's container file [pageWithGrid.container.js](/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.container.js) so the data and actions can be connected to the page props. 1. Map the data from the redux store to props. ```js const mapStateToProps = state => ({ @@ -38,11 +38,11 @@ Grids in remote monitoring are based on [ag-grid][ag-grid], with our own customi ``` 1. Connect the data and actions to the page component. ```js - export const GridExampleContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(GridExample)); + export const PageWithGridContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(PageWithGrid)); ``` - Notice the use of [i18next][i18next]'s translate method. This will pass an additional prop called `t` containing the translated strings for use in the page. -1. Open your page's file [gridExample.js](/src/components/pages/_gridExample/gridExample.js) so the grid and refresh bar can be added. +1. Open your page's file [pageWithGrid.js](/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.js) so the grid and refresh bar can be added. 1. Import your grid as well as other components like `AjaxError` and `RefreshBar`. ```js import { AjaxError, RefreshBar } from 'components/shared'; @@ -57,8 +57,8 @@ Grids in remote monitoring are based on [ag-grid][ag-grid], with our own customi if (!lastUpdated && !isPending) fetchData(); } ``` - - Alternatively, if the the data is useful on other pages as well, then it can be loaded in the "APP_INITIALIZE" epic in [appReducer.js](../store/reducers/appReducer.js) -1. In render, set up the props for the grid. Choose the columnDefs to show from those configured in [exampleGridConfig.js](/src/components/pages/_gridExample/exampleGrid/exampleGridConfig.js). + - Alternatively, if the the data is useful on other pages as well, then it can be loaded in the "APP_INITIALIZE" epic in [appReducer.js](/src/store/reducers/appReducer.js) +1. In render, set up the props for the grid. Choose the columnDefs to show from those configured in [exampleGridConfig.js](/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGridConfig.js). ```js const { t, data, error, isPending, lastUpdated, fetchData } = this.props; const gridProps = { @@ -135,7 +135,7 @@ The user may need to act on mulitple rows at the same time. Checking a row's che ### Soft Select Rows The user may need to act on a single row. A soft select link can be configured for one or more columns in the columnDefs. -1. In [exampleGridConfig.js](/src/components/pages/_gridExample/exampleGrid/exampleGridConfig.js), add `SoftSelectLinkRenderer` as the cellRendererFramework for a columnDef. +1. In [exampleGridConfig.js](/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGridConfig.js), add `SoftSelectLinkRenderer` as the cellRendererFramework for a columnDef. ```js export const exampleColumnDefs = { id: { diff --git a/docs/walkthrough/addNewPage.md b/docs/walkthrough/addNewPage.md index b9fa77cbc..a219226fd 100644 --- a/docs/walkthrough/addNewPage.md +++ b/docs/walkthrough/addNewPage.md @@ -1,47 +1,62 @@ Walkthrough: Adding a New Page =========================== -The following is for creating a new page called "**example**." +The following is for creating a new page called "**basicPage**." 1. Create a folder named `example` inside the `components/pages` folder. 1. Create 4 files in the new folder. See the individual example files for more details and comments inline. - - [example.container.js](/src/components/pages/_example/example.container.js) - maps redux and epic actions and selectors to the props for the page - - [example.js](/src/components/pages/_example/example.js) - main component for the page - - [example.scss](/src/components/pages/_example/example.scss) - styles for the page - - [example.test.js](/src/components/pages/_example/example.test.js) - basic rendering test -1. Add the new page's container to the [components/pages/index.js](/src/components/pages/index.js) file. + - [basicPage.container.js](/src/walkthrough/components/pages/basicPage/basicPage.container.js) - maps redux and epic actions and selectors to the props for the page + - [basicPage.js](/src/walkthrough/components/pages/basicPage/basicPage.js) - main component for the page + - [basicPage.scss](/src/walkthrough/components/pages/basicPage/basicPage.scss) - styles for the page + - [basicPage.test.js](/src/walkthrough/components/pages/basicPage/basicPage.test.js) - basic rendering test +1. Add the new page's container to the [components/pages/index.js](/src/walkthrough/components/pages/index.js) file. ```js - export * from './example/example.container'; + export * from './basicPage/basicPage.container'; ``` 1. (Optional) Add an SVG icon for the new page. See [utilities/README.md](../utilities/README.md) for more information. - Note that existing SVGs can be used as well. 1. Add the page name to the translations file, [translations.json](../../public/locales/en/translations.json). [i18next][i18next] is used for internationalization. ```json "tabs": { - "template": "Template", + "example": "Example", }, ``` -1. Open the top level application page, [components/app/app.js](/src/components/app/app.js). +1. Open the top level application page, [walkthrough/components/app.js](/src/components/app.js). 1. Add the new page to the imports. ```javascript // Page Components import { //... - TemplateContainer as TemplatePage + BasicPageContainer } from 'components/pages'; ``` -1. Add a navigation tab, refernce the SVG icon added previously. +1. Add the new page to the `pagesConfig`. Set the to address for the route, reference the SVG icon and translations added previously, and set the component to the page's container. ```js - const templateTab = { to: '/template', svg: svgs.tabs.template, labelId: 'tabs.template' }; + const pagesConfig = [ + //... + { + to: '/basicpage', + exact: true, + svg: svgs.tabs.example, + labelId: 'walkthrough.tabs.basicPage', + component: BasicPageContainer + }, + //... + ]; ``` -1. Add the new navigation tab to tabConfigs. +1. Add any new breadcrumbs to the `crumbsConfig`. ```js - const tabConfigs = [ dashboardTab, devicesTab, rulesTab, maintenanceTab, templateTab ]; + const crumbsConfig = [ + //... + { + path: '/basicpage', crumbs: [ + { to: '/basicpage', labelId: 'walkthrough.tabs.basicPage' } + ] + }, + //... + ]; ``` -1. In the render method, add the route for the new page. - ```jsx - - ``` + - This example page only has one breadcrumb to be shown, but some pages will have more. See [components/app.js](/src/components/app.js) for more examples. 1. **Congratulations!** Run the application to see your new page in action. 1. Now, you can edit the page to do what you want. diff --git a/docs/walkthrough/addNewService.md b/docs/walkthrough/addNewService.md index 13eaca548..c635263b4 100644 --- a/docs/walkthrough/addNewService.md +++ b/docs/walkthrough/addNewService.md @@ -7,16 +7,16 @@ Services in remote monitoring are called using [rxjs][rxjs] Observables. ### Create the Service -1. Create [exampleModels.js](/src/services/models/_exampleModels.js) for the service under the `services/models` folder. +1. Create [exampleModels.js](/src/walkthrough/services/models/exampleModels.js) for the service under the `services/models` folder. - See the models [README](/src/services/models/README.md) for more information on the purpose of these models and general naming conventions. - - Don't forget to add your new file to the exports in [index.js](/src/services/models/index.js) -1. Create [exampleService.js](/src/services/_exampleService.js) in the `services` folder. - - Use [services/httpClient.js](/src/services/httpClient.js) to make calls to the services. Then, transform the response using the models. + - Don't forget to add your new file to the exports in [index.js](/src/walkthrough/services/models/index.js) +1. Create [exampleService.js](/src/walkthrough/services/exampleService.js) in the `services` folder. + - Use [utilities/httpClient.js](/src/utilities/httpClient.js) to make calls to the services. Then, transform the response using the models. - Note that the example service does not call actual services. Instead, it returns hardcoded data as an observable to mimick service calls. - - Don't forget to add your new file to the exports in [index.js](/src/services/index.js) + - Don't forget to add your new file to the exports in [index.js](/src/walkthrough/services/index.js) ### Set up the Service in the store -1. Create [exampleReducer.js](/src/store/reducers/_exampleReducer.js) in the `store/reducers/` folder. +1. Create [exampleReducer.js](/src/walkthrough/store/reducers/exampleReducer.js) in the `store/reducers/` folder. 1. Add [redux-observable][redux-obs] epics to make service calls. ```js export const epics = createEpicScenario({ @@ -80,9 +80,9 @@ Services in remote monitoring are called using [rxjs][rxjs] Observables. ## Configure the Middleware -1. Add the reducer to the [rootReducer.js](/src/store/rootReducer.js) in the `store` folder. +1. Add the reducer to the [rootReducer.js](/src/walkthrough/store/rootReducer.js) in the `store` folder. ```js - import { reducer as exampleReducer } from './reducers/_exampleReducer'; + import { reducer as exampleReducer } from './reducers/exampleReducer'; const rootReducer = combineReducers({ ...exampleReducer, @@ -90,9 +90,9 @@ Services in remote monitoring are called using [rxjs][rxjs] Observables. }); ``` -1. Add the epics to [rootEpic.js](/src/store/rootEpic.js) in the `store` folder. +1. Add the epics to [rootEpic.js](/src/walkthrough/store/rootEpic.js) in the `store` folder. ```js - import { epics as exampleEpics } from './reducers/_exampleReducer'; + import { epics as exampleEpics } from './reducers/exampleReducer'; const epics = [ ...exampleEpics.getEpics(), diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 5f741db95..93d630fbf 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -17,10 +17,7 @@ "devices": "Devices", "rules": "Rules", "maintenance": "Maintenance", - "packages": "Packages", - "example": "Page Example", - "gridExample": "Grid Example", - "flyoutExample": "Flyout Example" + "packages": "Packages" }, "errorFormat": "Error: {{message}}", "errorCode": { @@ -583,15 +580,17 @@ "edgemanifest": "Edge Manifest" } }, - "examples": { - "pagePlaceholder": "This is a new page.", - "grid": { - "name": "Name", - "description": "Description", - "btn1": "Button 1", - "btn2": "Button 2" + "walkthrough": { + "tabs": { + "dashboard": "Dashboard", + "basicPage": "Basic Page", + "pageWithFlyout": "Page With Flyout", + "pageWithGrid": "Page With Grid" }, - "flyout": { + "basicPage": { + "pagePlaceholder": "This is a new page." + }, + "pageWithFlyout": { "title": "Flyout Example", "pageBody": "Click the context button above to open a flyout.", "open": "Open Flyout", @@ -610,6 +609,22 @@ } } }, + "pageWithGrid": { + "grid": { + "name": "Name", + "description": "Description", + "btn1": "Button 1", + "btn2": "Button 2" + } + }, + "dashboard": { + "panels": { + "example": { + "header": "Example Panel", + "panelBody": "This is a new panel." + } + } + }, "panel": { "header": "Example Panel", "panelBody": "This is a new panel." diff --git a/src/app.config.js b/src/app.config.js index ccf4072a6..f83340c4e 100644 --- a/src/app.config.js +++ b/src/app.config.js @@ -43,7 +43,11 @@ const Config = { closed: 'closed', acknowledged: 'acknowledged' }, - maxLogoFileSizeInBytes: 307200 + maxLogoFileSizeInBytes: 307200, + deviceType: { + simulated: 'Simulated', + physical: 'Physical' + } }; export default Config; diff --git a/src/components/app/app.container.js b/src/components/app.container.js similarity index 84% rename from src/components/app/app.container.js rename to src/components/app.container.js index aceaf4ee5..aff1c5938 100644 --- a/src/components/app/app.container.js +++ b/src/components/app.container.js @@ -22,6 +22,4 @@ const mapDispatchToProps = dispatch => ({ logout: () => AuthService.logout() }); -const AppContainer = withRouter(translate()(connect(mapStateToProps, mapDispatchToProps)(App))); - -export default AppContainer; +export const AppContainer = withRouter(translate()(connect(mapStateToProps, mapDispatchToProps)(App))); diff --git a/src/components/app.js b/src/components/app.js new file mode 100644 index 000000000..7f46ce9d4 --- /dev/null +++ b/src/components/app.js @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; + +import { svgs } from 'utilities'; +import Shell from "components/shell/shell"; +import { ManageDeviceGroupsContainer, SettingsContainer } from 'components/shell/flyouts'; +import { DashboardContainer, DevicesContainer, RulesContainer, MaintenanceContainer, PackagesContainer } from './pages'; + +class App extends Component { + + constructor(props) { + super(props); + + this.state = { openFlyout: '' }; + } + + closeFlyout = () => this.setState({ openFlyout: '' }); + + openSettings = () => this.setState({ openFlyout: 'settings' }); + + render() { + const { deviceGroupFlyoutIsOpen } = this.props; + const { openFlyout } = this.state; + + const pagesConfig = [ + { + to: '/dashboard', + exact: true, + svg: svgs.tabs.dashboard, + labelId: 'tabs.dashboard', + component: DashboardContainer + }, + { + to: '/devices', + exact: true, + svg: svgs.tabs.devices, + labelId: 'tabs.devices', + component: DevicesContainer + }, + { + to: '/rules', + exact: true, + svg: svgs.tabs.rules, + labelId: 'tabs.rules', + component: RulesContainer + }, + { + to: '/packages', + exact: true, + svg: svgs.tabs.packages, + labelId: 'tabs.packages', + component: PackagesContainer + }, + { + to: '/maintenance', + exact: false, + svg: svgs.tabs.maintenance, + labelId: 'tabs.maintenance', + component: MaintenanceContainer + } + ]; + + const crumbsConfig = [ + { + path: '/dashboard', crumbs: [ + { to: '/dashboard', labelId: 'tabs.dashboard' } + ] + }, + { + path: '/devices', crumbs: [ + { to: '/devices', labelId: 'tabs.devices' } + ] + }, + { + path: '/rules', crumbs: [ + { to: '/rules', labelId: 'tabs.rules' } + ] + }, + { + path: '/packages', crumbs: [ + { to: '/packages', labelId: 'tabs.packages' } + ] + }, + { + path: '/maintenance', crumbs: [ + { to: '/maintenance', labelId: 'tabs.maintenance' } + ] + }, + { + path: '/maintenance/notifications', crumbs: [ + { to: '/maintenance', labelId: 'tabs.maintenance' }, + { to: '/maintenance/notifications', labelId: 'maintenance.notifications' } + ] + }, + { + path: '/maintenance/rule/:id', crumbs: [ + { to: '/maintenance', labelId: 'tabs.maintenance' }, + { to: '/maintenance/notifications', labelId: 'maintenance.notifications' }, + { to: '/maintenance/rule/:id', matchParam: 'id' }, + ] + }, + { + path: '/maintenance/jobs', crumbs: [ + { to: '/maintenance', labelId: 'tabs.maintenance' }, + { to: '/maintenance/jobs', labelId: 'maintenance.jobs' } + ] + }, + { + path: '/maintenance/job/:id', crumbs: [ + { to: '/maintenance', labelId: 'tabs.maintenance' }, + { to: '/maintenance/jobs', labelId: 'maintenance.jobs' }, + { to: '/maintenance/rule/:id', matchParam: 'id' } + ] + } + ]; + + const shellProps = { + pagesConfig, + crumbsConfig, + openSettings: this.openSettings, + ...this.props + }; + + return ( + + {deviceGroupFlyoutIsOpen && } + {openFlyout === 'settings' && } + + ); + } +} + +export default App; diff --git a/src/components/app/app.js b/src/components/app/app.js deleted file mode 100644 index 002842633..000000000 --- a/src/components/app/app.js +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -import React, { Component } from 'react'; -import { Route, Redirect, Switch } from 'react-router-dom'; - -import Config from 'app.config'; -import { SettingsContainer, ManageDeviceGroupsContainer } from './flyouts'; - -// App Components -import Header from './header/header'; -import NavigationContainer from './navigation/navigationContainer'; -import Main from './main/main'; - -// Page Components -import { - DashboardContainer as DashboardPage, - DevicesContainer as DevicesPage, - RulesContainer as RulesPage, - MaintenanceContainer as MaintenancePage, - PackagesContainer as PackagesPage, - ExampleContainer as ExamplePage, - FlyoutExampleContainer as FlyoutExamplePage, - GridExampleContainer as GridExamplePage, - PageNotFoundContainer as PageNotFound -} from 'components/pages'; - -import { svgs } from 'utilities'; - -import './app.css'; - -/** The navigation tab configurations */ -const dashboardTab = { to: '/dashboard', svg: svgs.tabs.dashboard, labelId: 'tabs.dashboard' }; -const devicesTab = { to: '/devices', svg: svgs.tabs.devices, labelId: 'tabs.devices' }; -const rulesTab = { to: '/rules', svg: svgs.tabs.rules, labelId: 'tabs.rules' }; -const maintenanceTab = { to: '/maintenance', svg: svgs.tabs.maintenance, labelId: 'tabs.maintenance' }; -const packagesTab = { to: '/packages', svg: svgs.tabs.packages, labelId: 'tabs.packages' }; -const exampleTab = { to: '/example', svg: svgs.tabs.example, labelId: 'tabs.example' }; -const flyoutExampleTab = { to: '/flyoutexample', svg: svgs.tabs.example, labelId: 'tabs.flyoutExample' }; -const gridExampleTab = { to: '/gridexample', svg: svgs.tabs.example, labelId: 'tabs.gridExample' }; -const tabConfigs = [dashboardTab, devicesTab, rulesTab, packagesTab, maintenanceTab]; - -/** Only show example pages and components when configured to do so */ -if (Config.showWalkthroughExamples) { - tabConfigs.push(exampleTab); - tabConfigs.push(flyoutExampleTab); - tabConfigs.push(gridExampleTab); -} - -class WalkthroughExampleRoute extends Component { - render() { - const { component: Component, ...props } = this.props - - return ( - ( - Config.showWalkthroughExamples ? - : - - )} - /> - ) - } -} - -/** The base component for the app */ -class App extends Component { - - constructor(props) { - super(props); - - this.state = { openFlyout: '' }; - } - - componentDidMount() { - const { history, registerRouteEvent } = this.props; - // Initialize listener to inject the route change event into the epic action stream - history.listen(({ pathname }) => registerRouteEvent(pathname)); - } - - closeFlyout = () => this.setState({ openFlyout: '' }); - - openSettings = () => this.setState({ openFlyout: 'settings' }); - - render() { - return ( -
-
- -
-
- - - - - - - - - - - - - {this.props.deviceGroupFlyoutIsOpen && } - {this.state.openFlyout === 'settings' && } -
-
-
- ); - } -} - -export default App; diff --git a/src/components/app/header/breadcrumbs.js b/src/components/app/header/breadcrumbs.js deleted file mode 100644 index 076458c81..000000000 --- a/src/components/app/header/breadcrumbs.js +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -import React from 'react'; -import { Route, Switch, NavLink } from 'react-router-dom'; - -import { Svg } from 'components/shared'; -import { svgs } from 'utilities'; - -const Crumb = ({ children }) =>
{ children }
-const Chevron = () => ; - -const DashboardCrumbs = ({ t }) => {t('tabs.dashboard')}; -const DevicesCrumbs = ({ t }) => {t('tabs.devices')}; -const RulesCrumbs = ({ t }) => {t('tabs.rules')}; -const MaintenanceCrumbs = ({ t }) => {t('tabs.maintenance')}; -const RuleDetailsCrumbs = ({ t, match }) => [ - {t('tabs.maintenance')}, - , - {match.params.id} -]; -const JobDetailsCrumbs = ({ t, match }) => [ - {t('tabs.maintenance')}, - , - {match.params.id} -]; - -export const Breadcrumbs = ({ t }) => ( - - } /> - } /> - } /> - } /> - } /> - } /> - -); diff --git a/src/components/pages/_example/example.container.js b/src/components/pages/_example/example.container.js deleted file mode 100644 index 6bd7e95bf..000000000 --- a/src/components/pages/_example/example.container.js +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -import { translate } from 'react-i18next'; - -import { Example } from './example'; - -export const ExampleContainer = translate()(Example); diff --git a/src/components/pages/_flyoutExample/flyoutExample.container.js b/src/components/pages/_flyoutExample/flyoutExample.container.js deleted file mode 100644 index 9ee5abb7d..000000000 --- a/src/components/pages/_flyoutExample/flyoutExample.container.js +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -import { translate } from 'react-i18next'; - -import { FlyoutExample } from './flyoutExample'; - -export const FlyoutExampleContainer = translate()(FlyoutExample); diff --git a/src/components/pages/dashboard/dashboard.js b/src/components/pages/dashboard/dashboard.js index 330b51816..78b32c7e4 100644 --- a/src/components/pages/dashboard/dashboard.js +++ b/src/components/pages/dashboard/dashboard.js @@ -5,13 +5,13 @@ import { Observable, Subject } from 'rxjs'; import moment from 'moment'; import Config from 'app.config'; -import { TelemetryService, retryHandler } from 'services'; -import { compareByProperty, getIntervalParams } from 'utilities'; +import { TelemetryService } from 'services'; +import { compareByProperty, getIntervalParams, retryHandler } from 'utilities'; import { Grid, Cell } from './grid'; import { PanelErrorBoundary } from './panel'; -import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/app/deviceGroupDropdown'; -import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn'; -import { TimeIntervalDropdown } from 'components/app/timeIntervalDropdown'; +import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/shell/deviceGroupDropdown'; +import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/shell/manageDeviceGroupsBtn'; +import { TimeIntervalDropdown } from 'components/shell/timeIntervalDropdown'; import { OverviewPanel, AlertsPanel, diff --git a/src/components/pages/devices/devices.container.js b/src/components/pages/devices/devices.container.js index 3db7b66e1..3b16f281b 100644 --- a/src/components/pages/devices/devices.container.js +++ b/src/components/pages/devices/devices.container.js @@ -12,6 +12,7 @@ import { } from 'store/reducers/devicesReducer'; import { redux as appRedux, + epics as appEpics, getDeviceGroups, getDeviceGroupError } from 'store/reducers/appReducer'; @@ -29,7 +30,8 @@ const mapStateToProps = state => ({ // Wrap the dispatch method const mapDispatchToProps = dispatch => ({ fetchDevices: () => dispatch(devicesEpics.actions.fetchDevices()), - updateCurrentWindow: (currentWindow) => dispatch(appRedux.actions.updateCurrentWindow(currentWindow)) + updateCurrentWindow: (currentWindow) => dispatch(appRedux.actions.updateCurrentWindow(currentWindow)), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const DevicesContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Devices)); diff --git a/src/components/pages/devices/devices.js b/src/components/pages/devices/devices.js index 39e622a0d..f5e779aad 100644 --- a/src/components/pages/devices/devices.js +++ b/src/components/pages/devices/devices.js @@ -2,10 +2,10 @@ import React, { Component } from 'react'; -import { permissions } from 'services/models'; +import { permissions, toDiagnosticsModel } from 'services/models'; import { DevicesGrid } from './devicesGrid'; -import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/app/deviceGroupDropdown'; -import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn'; +import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/shell/deviceGroupDropdown'; +import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/shell/manageDeviceGroupsBtn'; import { AjaxError, Btn, @@ -45,7 +45,10 @@ export class Devices extends Component { closeFlyout = () => this.setState(closedFlyoutState); openSIMManagement = () => this.setState({ openFlyoutName: 'sim-management' }); - openNewDeviceFlyout = () => this.setState({ openFlyoutName: 'new-device' }); + openNewDeviceFlyout = () => { + this.setState({ openFlyoutName: 'new-device' }); + this.props.logEvent(toDiagnosticsModel('Devices_NewClick', {})); + } onContextMenuChange = contextBtns => this.setState({ contextBtns, diff --git a/src/components/pages/devices/flyouts/deviceNew/deviceNew.container.js b/src/components/pages/devices/flyouts/deviceNew/deviceNew.container.js index 939b0c337..7756e5276 100644 --- a/src/components/pages/devices/flyouts/deviceNew/deviceNew.container.js +++ b/src/components/pages/devices/flyouts/deviceNew/deviceNew.container.js @@ -11,6 +11,9 @@ import { epics as devicesEpics, redux as devicesRedux } from 'store/reducers/devicesReducer'; +import { + epics as appEpics, +} from 'store/reducers/appReducer'; // Pass the global info needed const mapStateToProps = state => ({ @@ -21,7 +24,8 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ fetchDeviceModelOptions: () => dispatch(simulationEpics.actions.fetchSimulationDeviceModelOptions()), insertDevices: devices => dispatch(devicesRedux.actions.insertDevices(devices)), - fetchDevices: () => dispatch(devicesEpics.actions.fetchDevices()) + fetchDevices: () => dispatch(devicesEpics.actions.fetchDevices()), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const DeviceNewContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeviceNew)); diff --git a/src/components/pages/devices/flyouts/deviceNew/deviceNew.js b/src/components/pages/devices/flyouts/deviceNew/deviceNew.js index 94f170701..74c5950d9 100644 --- a/src/components/pages/devices/flyouts/deviceNew/deviceNew.js +++ b/src/components/pages/devices/flyouts/deviceNew/deviceNew.js @@ -4,7 +4,14 @@ import React from 'react'; import update from 'immutability-helper'; import { DeviceSimulationService, IoTHubManagerService } from 'services'; -import { authenticationTypeOptions, permissions, toNewDeviceRequestModel } from 'services/models'; +import { + authenticationTypeOptions, + permissions, + toNewDeviceRequestModel, + toSinglePropertyDiagnosticsModel, + toDeviceDiagnosticsModel, + toDiagnosticsModel +} from 'services/models'; import { copyToClipboard, int, @@ -39,6 +46,7 @@ import { } from 'components/shared'; import './deviceNew.css'; +import Config from 'app.config'; const isIntRegex = /^-?\d*$/; const nonInteger = x => !x.match(isIntRegex); @@ -242,6 +250,12 @@ export class DeviceNew extends LinkedComponent { ].every(link => !link.error); } + deviceTypeChange = ({ target: { value }}) => { + this.props.logEvent(toSinglePropertyDiagnosticsModel('Devices_DeviceTypeSelect', 'DeviceType', + (value === 'true') ? Config.deviceType.simulated: Config.deviceType.physical)); + this.formControlChange(); + } + formControlChange = () => { if (this.state.changesApplied) { this.setState({ @@ -252,6 +266,11 @@ export class DeviceNew extends LinkedComponent { } } + onFlyoutClose = (eventName) => { + this.props.logEvent(toDeviceDiagnosticsModel(eventName, this.state.formData)); + this.props.onClose(); + } + apply = (event) => { event.preventDefault(); const { formData } = this.state; @@ -261,11 +280,14 @@ export class DeviceNew extends LinkedComponent { if (this.provisionSubscription) this.provisionSubscription.unsubscribe(); + this.props.logEvent(toDeviceDiagnosticsModel('Devices_ApplyClick', formData)); + if (this.state.formData.isSimulated) { this.provisionSubscription = DeviceSimulationService.incrementSimulatedDeviceModel(formData.deviceModel, formData.count) .subscribe( () => { this.setState({ successCount: formData.count, isPending: false, changesApplied: true }); + this.props.logEvent(toSinglePropertyDiagnosticsModel('Devices_Created', 'DeviceType', Config.deviceType.simulated)); }, error => { this.setState({ error, isPending: false, changesApplied: true }); @@ -278,6 +300,11 @@ export class DeviceNew extends LinkedComponent { provisionedDevice => { this.setState({ provisionedDevice, successCount: formData.count, isPending: false, changesApplied: true }); this.props.insertDevices([provisionedDevice]); + const metadata = { + DeviceType: Config.deviceType.physical, + DeviceID: provisionedDevice.id + }; + this.props.logEvent(toDiagnosticsModel('Devices_Created', metadata)); }, error => { this.setState({ error, isPending: false, changesApplied: true }); @@ -303,7 +330,6 @@ export class DeviceNew extends LinkedComponent { render() { const { t, - onClose, deviceModelOptions } = this.props; const { @@ -328,7 +354,7 @@ export class DeviceNew extends LinkedComponent { {t('devices.flyouts.new.title')} - + this.onFlyoutClose('Devices_TopXCloseClick')} /> @@ -336,10 +362,10 @@ export class DeviceNew extends LinkedComponent {
{t(deviceTypeOptions.labelName)} - + {t(deviceTypeOptions.simulated.labelName)} - + {t(deviceTypeOptions.physical.labelName)} @@ -421,14 +447,14 @@ export class DeviceNew extends LinkedComponent { !changesApplied && {t('devices.flyouts.new.apply')} - {t('devices.flyouts.new.cancel')} + this.onFlyoutClose('Devices_CancelClick')}>{t('devices.flyouts.new.cancel')} } { !!changesApplied && [ , - {t('devices.flyouts.new.close')} + this.onFlyoutClose('Devices_CloseClick')}>{t('devices.flyouts.new.close')} ] } diff --git a/src/components/pages/index.js b/src/components/pages/index.js index 121e50c59..f41810530 100644 --- a/src/components/pages/index.js +++ b/src/components/pages/index.js @@ -1,14 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. // Exports the app page components - -export * from './_example/example.container'; -export * from './_flyoutExample/flyoutExample.container'; -export * from './_gridExample/gridExample.container'; export * from './dashboard/dashboard.container'; export * from './devices/devices.container'; export * from './rules/rules.container'; export * from './maintenance/maintenance.container'; -export * from './pageNotFound/pageNotFound'; -export * from './pageNotFound/pageNotFound.container'; export * from './packages/packages.container'; diff --git a/src/components/pages/maintenance/jobDetails/jobDetails.js b/src/components/pages/maintenance/jobDetails/jobDetails.js index b619c9a11..6114c7014 100644 --- a/src/components/pages/maintenance/jobDetails/jobDetails.js +++ b/src/components/pages/maintenance/jobDetails/jobDetails.js @@ -6,7 +6,7 @@ import Config from 'app.config'; import { AjaxError, PageContent, ContextMenu, RefreshBar } from 'components/shared'; import { DevicesGrid } from 'components/pages/devices/devicesGrid'; import { JobGrid, JobStatusGrid } from 'components/pages/maintenance/grids'; -import { TimeIntervalDropdown } from 'components/app/timeIntervalDropdown'; +import { TimeIntervalDropdown } from 'components/shell/timeIntervalDropdown'; import { IoTHubManagerService } from 'services'; diff --git a/src/components/pages/maintenance/ruleDetails/ruleDetails.js b/src/components/pages/maintenance/ruleDetails/ruleDetails.js index 8c9429679..912431dd1 100644 --- a/src/components/pages/maintenance/ruleDetails/ruleDetails.js +++ b/src/components/pages/maintenance/ruleDetails/ruleDetails.js @@ -18,9 +18,9 @@ import { } from 'components/shared'; import { svgs, joinClasses, renderUndefined } from 'utilities'; import { DevicesGrid } from 'components/pages/devices/devicesGrid'; -import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/app/deviceGroupDropdown'; -import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn'; -import { TimeIntervalDropdown } from 'components/app/timeIntervalDropdown'; +import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/shell/deviceGroupDropdown'; +import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/shell/manageDeviceGroupsBtn'; +import { TimeIntervalDropdown } from 'components/shell/timeIntervalDropdown'; import { TelemetryChart, transformTelemetryResponse, chartColorObjects } from 'components/pages/dashboard/panels/telemetry'; import { TelemetryService } from 'services'; import { TimeRenderer, SeverityRenderer } from 'components/shared/cellRenderers'; diff --git a/src/components/pages/maintenance/summary/summary.js b/src/components/pages/maintenance/summary/summary.js index 0a0574ff3..d602834ee 100644 --- a/src/components/pages/maintenance/summary/summary.js +++ b/src/components/pages/maintenance/summary/summary.js @@ -5,9 +5,9 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { NavLink } from 'react-router-dom'; -import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/app/deviceGroupDropdown'; -import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn'; -import { TimeIntervalDropdown } from 'components/app/timeIntervalDropdown'; +import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/shell/deviceGroupDropdown'; +import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/shell/manageDeviceGroupsBtn'; +import { TimeIntervalDropdown } from 'components/shell/timeIntervalDropdown'; import { Notifications } from './notifications'; import { Jobs } from './jobs'; import { diff --git a/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js b/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js index ff4a9cab1..7ef05576e 100644 --- a/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js +++ b/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js @@ -147,6 +147,7 @@ export class RuleEditor extends LinkedComponent { erroe: undefined } }; + logEvent(toRuleDiagnosticsModel('Rule_ApplyClick', requestProps)); if (this.props.rule) { // If rule object exist then update the existing rule this.subscription = TelemetryService.updateRule(this.props.rule.id, toNewRuleRequestModel(requestProps)) .subscribe( @@ -165,7 +166,6 @@ export class RuleEditor extends LinkedComponent { }, error => this.setState({ error, isPending: false, changesApplied: true }) ); - logEvent(toRuleDiagnosticsModel('Rule_ApplyClick', requestProps)); } } } diff --git a/src/components/pages/rules/rules.js b/src/components/pages/rules/rules.js index bea5ef561..31deedf75 100644 --- a/src/components/pages/rules/rules.js +++ b/src/components/pages/rules/rules.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import { permissions, toDiagnosticsModel } from 'services/models'; import { RulesGrid } from './rulesGrid'; -import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/app/deviceGroupDropdown'; -import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn'; +import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/shell/deviceGroupDropdown'; +import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/shell/manageDeviceGroupsBtn'; import { AjaxError, Btn, diff --git a/src/components/app/deviceGroupDropdown/deviceGroupDropdown.container.js b/src/components/shell/deviceGroupDropdown/deviceGroupDropdown.container.js similarity index 100% rename from src/components/app/deviceGroupDropdown/deviceGroupDropdown.container.js rename to src/components/shell/deviceGroupDropdown/deviceGroupDropdown.container.js diff --git a/src/components/app/deviceGroupDropdown/deviceGroupDropdown.js b/src/components/shell/deviceGroupDropdown/deviceGroupDropdown.js similarity index 100% rename from src/components/app/deviceGroupDropdown/deviceGroupDropdown.js rename to src/components/shell/deviceGroupDropdown/deviceGroupDropdown.js diff --git a/src/components/app/deviceGroupDropdown/deviceGroupDropdown.scss b/src/components/shell/deviceGroupDropdown/deviceGroupDropdown.scss similarity index 100% rename from src/components/app/deviceGroupDropdown/deviceGroupDropdown.scss rename to src/components/shell/deviceGroupDropdown/deviceGroupDropdown.scss diff --git a/src/components/app/deviceGroupDropdown/index.js b/src/components/shell/deviceGroupDropdown/index.js similarity index 100% rename from src/components/app/deviceGroupDropdown/index.js rename to src/components/shell/deviceGroupDropdown/index.js diff --git a/src/components/app/flyouts/index.js b/src/components/shell/flyouts/index.js similarity index 100% rename from src/components/app/flyouts/index.js rename to src/components/shell/flyouts/index.js diff --git a/src/components/app/flyouts/manageDeviceGroups/index.js b/src/components/shell/flyouts/manageDeviceGroups/index.js similarity index 100% rename from src/components/app/flyouts/manageDeviceGroups/index.js rename to src/components/shell/flyouts/manageDeviceGroups/index.js diff --git a/src/components/app/flyouts/manageDeviceGroups/manageDeviceGroups.container.js b/src/components/shell/flyouts/manageDeviceGroups/manageDeviceGroups.container.js similarity index 100% rename from src/components/app/flyouts/manageDeviceGroups/manageDeviceGroups.container.js rename to src/components/shell/flyouts/manageDeviceGroups/manageDeviceGroups.container.js diff --git a/src/components/app/flyouts/manageDeviceGroups/manageDeviceGroups.js b/src/components/shell/flyouts/manageDeviceGroups/manageDeviceGroups.js similarity index 100% rename from src/components/app/flyouts/manageDeviceGroups/manageDeviceGroups.js rename to src/components/shell/flyouts/manageDeviceGroups/manageDeviceGroups.js diff --git a/src/components/app/flyouts/manageDeviceGroups/manageDeviceGroups.scss b/src/components/shell/flyouts/manageDeviceGroups/manageDeviceGroups.scss similarity index 100% rename from src/components/app/flyouts/manageDeviceGroups/manageDeviceGroups.scss rename to src/components/shell/flyouts/manageDeviceGroups/manageDeviceGroups.scss diff --git a/src/components/app/flyouts/manageDeviceGroups/views/deviceGroupForm.js b/src/components/shell/flyouts/manageDeviceGroups/views/deviceGroupForm.js similarity index 100% rename from src/components/app/flyouts/manageDeviceGroups/views/deviceGroupForm.js rename to src/components/shell/flyouts/manageDeviceGroups/views/deviceGroupForm.js diff --git a/src/components/app/flyouts/manageDeviceGroups/views/deviceGroups.js b/src/components/shell/flyouts/manageDeviceGroups/views/deviceGroups.js similarity index 100% rename from src/components/app/flyouts/manageDeviceGroups/views/deviceGroups.js rename to src/components/shell/flyouts/manageDeviceGroups/views/deviceGroups.js diff --git a/src/components/app/flyouts/settings/applicationSettings.js b/src/components/shell/flyouts/settings/applicationSettings.js similarity index 100% rename from src/components/app/flyouts/settings/applicationSettings.js rename to src/components/shell/flyouts/settings/applicationSettings.js diff --git a/src/components/app/flyouts/settings/applicationSettings.scss b/src/components/shell/flyouts/settings/applicationSettings.scss similarity index 100% rename from src/components/app/flyouts/settings/applicationSettings.scss rename to src/components/shell/flyouts/settings/applicationSettings.scss diff --git a/src/components/app/flyouts/settings/index.js b/src/components/shell/flyouts/settings/index.js similarity index 100% rename from src/components/app/flyouts/settings/index.js rename to src/components/shell/flyouts/settings/index.js diff --git a/src/components/app/flyouts/settings/settings.container.js b/src/components/shell/flyouts/settings/settings.container.js similarity index 100% rename from src/components/app/flyouts/settings/settings.container.js rename to src/components/shell/flyouts/settings/settings.container.js diff --git a/src/components/app/flyouts/settings/settings.js b/src/components/shell/flyouts/settings/settings.js similarity index 99% rename from src/components/app/flyouts/settings/settings.js rename to src/components/shell/flyouts/settings/settings.js index a7e9eaa49..c25fe16ab 100644 --- a/src/components/app/flyouts/settings/settings.js +++ b/src/components/shell/flyouts/settings/settings.js @@ -6,7 +6,7 @@ import Config from 'app.config'; import Flyout from 'components/shared/flyout'; import { Btn, Indicator, ToggleBtn } from 'components/shared'; import { svgs, LinkedComponent, isDef } from 'utilities'; -import ApplicationSettings from 'components/app/flyouts/settings/applicationSettings'; +import ApplicationSettings from './applicationSettings'; import './settings.css'; diff --git a/src/components/app/flyouts/settings/settings.scss b/src/components/shell/flyouts/settings/settings.scss similarity index 100% rename from src/components/app/flyouts/settings/settings.scss rename to src/components/shell/flyouts/settings/settings.scss diff --git a/src/components/shell/header/breadcrumbs.js b/src/components/shell/header/breadcrumbs.js new file mode 100644 index 000000000..9dde2334d --- /dev/null +++ b/src/components/shell/header/breadcrumbs.js @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; +import { Route, Switch, NavLink } from 'react-router-dom'; + +import { Svg } from 'components/shared'; +import { svgs, isDef } from 'utilities'; + +const Crumb = ({ children }) =>
{children}
+const Chevron = () => ; + +const CrumbFromConfig = ({ t, crumb, match, isLast }) => { + + const separator = ; + + if (isDef(crumb.labelId) && !isDef(crumb.matchParam)) { + if (!isLast) { + return [{t(crumb.labelId)}, separator] + } else { + return {t(crumb.labelId)} + } + } else if (!isDef(crumb.labelId) && isDef(crumb.matchParam)) { + return {match.params[crumb.matchParam]} + } +} + +export const Breadcrumbs = ({ t, crumbsConfig }) => ( + + { + crumbsConfig.map(({ path, crumbs }) => + { + return crumbs.map((crumb, idx) => ); + }} /> + ) + } + +); diff --git a/src/components/app/header/header.js b/src/components/shell/header/header.js similarity index 97% rename from src/components/app/header/header.js rename to src/components/shell/header/header.js index 7008a36e4..187d5066f 100644 --- a/src/components/app/header/header.js +++ b/src/components/shell/header/header.js @@ -75,7 +75,7 @@ class Header extends Component { return (
- +
{ this.props.t('header.appName') }
diff --git a/src/components/app/header/header.scss b/src/components/shell/header/header.scss similarity index 100% rename from src/components/app/header/header.scss rename to src/components/shell/header/header.scss diff --git a/src/components/app/main/main.js b/src/components/shell/main/main.js similarity index 100% rename from src/components/app/main/main.js rename to src/components/shell/main/main.js diff --git a/src/components/app/main/main.scss b/src/components/shell/main/main.scss similarity index 100% rename from src/components/app/main/main.scss rename to src/components/shell/main/main.scss diff --git a/src/components/app/manageDeviceGroupsBtn/index.js b/src/components/shell/manageDeviceGroupsBtn/index.js similarity index 100% rename from src/components/app/manageDeviceGroupsBtn/index.js rename to src/components/shell/manageDeviceGroupsBtn/index.js diff --git a/src/components/app/manageDeviceGroupsBtn/manageDeviceGroupsBtn.container.js b/src/components/shell/manageDeviceGroupsBtn/manageDeviceGroupsBtn.container.js similarity index 100% rename from src/components/app/manageDeviceGroupsBtn/manageDeviceGroupsBtn.container.js rename to src/components/shell/manageDeviceGroupsBtn/manageDeviceGroupsBtn.container.js diff --git a/src/components/app/manageDeviceGroupsBtn/manageDeviceGroupsBtn.js b/src/components/shell/manageDeviceGroupsBtn/manageDeviceGroupsBtn.js similarity index 100% rename from src/components/app/manageDeviceGroupsBtn/manageDeviceGroupsBtn.js rename to src/components/shell/manageDeviceGroupsBtn/manageDeviceGroupsBtn.js diff --git a/src/components/app/navigation/navigation.js b/src/components/shell/navigation/navigation.js similarity index 100% rename from src/components/app/navigation/navigation.js rename to src/components/shell/navigation/navigation.js diff --git a/src/components/app/navigation/navigation.scss b/src/components/shell/navigation/navigation.scss similarity index 100% rename from src/components/app/navigation/navigation.scss rename to src/components/shell/navigation/navigation.scss diff --git a/src/components/app/navigation/navigationContainer.js b/src/components/shell/navigation/navigationContainer.js similarity index 100% rename from src/components/app/navigation/navigationContainer.js rename to src/components/shell/navigation/navigationContainer.js diff --git a/src/components/shell/pageNotFound/index.js b/src/components/shell/pageNotFound/index.js new file mode 100644 index 000000000..78dfbc6cf --- /dev/null +++ b/src/components/shell/pageNotFound/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './pageNotFound'; +export * from './pageNotFound.container'; diff --git a/src/components/pages/pageNotFound/pageNotFound.container.js b/src/components/shell/pageNotFound/pageNotFound.container.js similarity index 100% rename from src/components/pages/pageNotFound/pageNotFound.container.js rename to src/components/shell/pageNotFound/pageNotFound.container.js diff --git a/src/components/pages/pageNotFound/pageNotFound.js b/src/components/shell/pageNotFound/pageNotFound.js similarity index 100% rename from src/components/pages/pageNotFound/pageNotFound.js rename to src/components/shell/pageNotFound/pageNotFound.js diff --git a/src/components/pages/pageNotFound/pageNotFound.scss b/src/components/shell/pageNotFound/pageNotFound.scss similarity index 100% rename from src/components/pages/pageNotFound/pageNotFound.scss rename to src/components/shell/pageNotFound/pageNotFound.scss diff --git a/src/components/shell/shell.container.js b/src/components/shell/shell.container.js new file mode 100644 index 000000000..03d1487be --- /dev/null +++ b/src/components/shell/shell.container.js @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { AuthService } from 'services'; +import { + epics as appEpics, + getTheme, + getDeviceGroupFlyoutStatus +} from 'store/reducers/appReducer'; +import Shell from './shell'; + +const mapStateToProps = state => ({ + theme: getTheme(state), + deviceGroupFlyoutIsOpen: getDeviceGroupFlyoutStatus(state) +}); + +// Wrap with the router and wrap the dispatch method +const mapDispatchToProps = dispatch => ({ + registerRouteEvent: pathname => dispatch(appEpics.actions.detectRouteChange(pathname)), + logout: () => AuthService.logout() +}); + +export const ShellContainer = withRouter(translate()(connect(mapStateToProps, mapDispatchToProps)(Shell))); diff --git a/src/components/shell/shell.js b/src/components/shell/shell.js new file mode 100644 index 000000000..b800e8685 --- /dev/null +++ b/src/components/shell/shell.js @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; +import { Route, Redirect, Switch } from 'react-router-dom'; + +// App Components +import Header from './header/header'; +import NavigationContainer from './navigation/navigationContainer'; +import Main from './main/main'; +import { PageNotFoundContainer as PageNotFound } from './pageNotFound' + +import './shell.css'; + +/** The base component for the app shell */ +class Shell extends Component { + + constructor(props) { + super(props); + + this.state = { openFlyout: '' }; + } + + componentDidMount() { + const { history, registerRouteEvent } = this.props; + // Initialize listener to inject the route change event into the epic action stream + history.listen(({ pathname }) => registerRouteEvent(pathname)); + } + + render() { + const { pagesConfig, crumbsConfig, openSettings, logout, t, theme, children } = this.props; + + return ( +
+ { + pagesConfig && +
+ +
+
+ + + { + pagesConfig.map(({ to, exact, component }) => + ) + } + + + {children} +
+
+ } +
+ ); + } +} + +export default Shell; diff --git a/src/components/app/app.scss b/src/components/shell/shell.scss similarity index 96% rename from src/components/app/app.scss rename to src/components/shell/shell.scss index 13d78f8c5..522c7af9c 100644 --- a/src/components/app/app.scss +++ b/src/components/shell/shell.scss @@ -3,9 +3,9 @@ @import 'src/styles/themes'; @import 'src/styles/mixins'; -.app-container { height: 100%; } +.shell-container { height: 100%; } -.app { +.shell { height: 100%; display: flex; diff --git a/src/components/app/app.test.js b/src/components/shell/shell.test.js similarity index 60% rename from src/components/app/app.test.js rename to src/components/shell/shell.test.js index 9a85395e4..daf402328 100644 --- a/src/components/app/app.test.js +++ b/src/components/shell/shell.test.js @@ -4,15 +4,15 @@ import React from 'react'; import { mount } from 'enzyme'; import 'polyfills'; -import AppContainer from 'components/app/app.container'; +import ShellContainer from 'components/shell/shell.container'; import MockApp from 'components/mocks/mockApp'; -describe('App integration test', () => { +describe('Shell integration test', () => { let wrapper; - it('Render App component', () => { + it('Render Shell component', () => { wrapper = mount( - + ); }); diff --git a/src/components/app/timeIntervalDropdown/index.js b/src/components/shell/timeIntervalDropdown/index.js similarity index 100% rename from src/components/app/timeIntervalDropdown/index.js rename to src/components/shell/timeIntervalDropdown/index.js diff --git a/src/components/app/timeIntervalDropdown/timeIntervalDropdown.js b/src/components/shell/timeIntervalDropdown/timeIntervalDropdown.js similarity index 100% rename from src/components/app/timeIntervalDropdown/timeIntervalDropdown.js rename to src/components/shell/timeIntervalDropdown/timeIntervalDropdown.js diff --git a/src/components/app/timeIntervalDropdown/timeIntervalDropdown.scss b/src/components/shell/timeIntervalDropdown/timeIntervalDropdown.scss similarity index 100% rename from src/components/app/timeIntervalDropdown/timeIntervalDropdown.scss rename to src/components/shell/timeIntervalDropdown/timeIntervalDropdown.scss diff --git a/src/index.js b/src/index.js index 75da4b1b3..3e9f4078b 100644 --- a/src/index.js +++ b/src/index.js @@ -5,8 +5,11 @@ import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; -import configureStore from 'store/configureStore'; -import AppContainer from 'components/app/app.container'; +import Config from 'app.config'; +import { configureStore } from 'store/configureStore'; +import { AppContainer as App } from 'components/app.container'; +import { configureStore as configureWalkthroughStore } from 'walkthrough/store/configureStore'; +import { AppContainer as WalkthroughApp } from 'walkthrough/components/app.container'; import registerServiceWorker from 'registerServiceWorker'; import { AuthService } from 'services'; import { epics as appEpics } from 'store/reducers/appReducer'; @@ -23,7 +26,9 @@ import './index.css'; // Initialize the user authentication AuthService.onLoad(() => { // Create the redux store and redux-observable streams - const store = configureStore(); + const store = Config.showWalkthroughExamples + ? configureWalkthroughStore() + : configureStore(); // Initialize the app redux state store.dispatch(appEpics.actions.initializeApp()); @@ -32,7 +37,10 @@ AuthService.onLoad(() => { ReactDOM.render( - + {Config.showWalkthroughExamples + ? + : + } , document.getElementById('root') diff --git a/src/services/authService.js b/src/services/authService.js index 7e6306147..06a2fad55 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -3,7 +3,7 @@ import Config from 'app.config'; import AuthenticationContext from 'adal-angular/dist/adal.min.js' import { Observable } from 'rxjs'; -import { HttpClient } from './httpClient'; +import { HttpClient } from 'utilities/httpClient'; import { toUserModel, authDisabledUser } from './models'; const ENDPOINT = Config.serviceUrls.auth; diff --git a/src/services/configService.js b/src/services/configService.js index d01b3d61a..c8f5117f0 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import Config from 'app.config'; -import { HttpClient } from './httpClient'; +import { HttpClient } from 'utilities/httpClient'; import { prepareLogoResponse, toDeviceGroupModel, diff --git a/src/services/deviceSimulationService.js b/src/services/deviceSimulationService.js index 2ff13de73..05d9c999f 100644 --- a/src/services/deviceSimulationService.js +++ b/src/services/deviceSimulationService.js @@ -3,7 +3,7 @@ import { Observable } from 'rxjs'; import Config from 'app.config'; -import { HttpClient } from './httpClient'; +import { HttpClient } from 'utilities/httpClient'; import { toDeviceModelSelectOptions, toDeviceSimulationModel, toDeviceSimulationRequestModel } from './models'; const ENDPOINT = Config.serviceUrls.deviceSimulation; diff --git a/src/services/diagnosticsService.js b/src/services/diagnosticsService.js index 29a0bdc4b..53ed2c6a8 100644 --- a/src/services/diagnosticsService.js +++ b/src/services/diagnosticsService.js @@ -1,5 +1,5 @@ import Config from 'app.config'; -import { HttpClient } from './httpClient'; +import { HttpClient } from 'utilities/httpClient'; import { toDiagnosticsRequestModel } from './models'; const ENDPOINT = Config.serviceUrls.diagnostics; diff --git a/src/services/gitHubService.js b/src/services/gitHubService.js index b56098822..70163f3a0 100644 --- a/src/services/gitHubService.js +++ b/src/services/gitHubService.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import Config from 'app.config'; -import { HttpClient } from './httpClient'; +import { HttpClient } from 'utilities/httpClient'; import { toGitHubModel } from './models'; diff --git a/src/services/index.js b/src/services/index.js index 985bdd785..75b6ade97 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -2,10 +2,8 @@ // Exports the shared react components into as a library -export * from './_exampleService'; export * from './authService'; export * from './deviceSimulationService'; -export * from './httpClient'; export * from './iotHubManagerService'; export * from './telemetryService'; export * from './configService'; diff --git a/src/services/iotHubManagerService.js b/src/services/iotHubManagerService.js index 39b946642..c9998dff0 100644 --- a/src/services/iotHubManagerService.js +++ b/src/services/iotHubManagerService.js @@ -4,7 +4,7 @@ import { Observable } from 'rxjs'; import Config from 'app.config'; import { stringify } from 'query-string'; -import { HttpClient } from './httpClient'; +import { HttpClient } from 'utilities/httpClient'; import { toDevicesModel, toDeviceModel, toJobsModel, toJobStatusModel, toDevicePropertiesModel } from './models'; const ENDPOINT = Config.serviceUrls.iotHubManager; diff --git a/src/services/models/index.js b/src/services/models/index.js index ad766ed6e..753079aa8 100644 --- a/src/services/models/index.js +++ b/src/services/models/index.js @@ -2,8 +2,6 @@ // Exports models -export * from './_exampleModels'; -export * from './ajaxModels'; export * from './authModels'; export * from './deviceSimulationModels'; export * from './iotHubManagerModels'; diff --git a/src/services/models/logEventModels.js b/src/services/models/logEventModels.js index 9ece4eec8..0bdb511e7 100644 --- a/src/services/models/logEventModels.js +++ b/src/services/models/logEventModels.js @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import { toDiagnosticsModel } from 'services/models'; +import Config from 'app.config'; export const toRuleDiagnosticsModel = (eventName, rule) => { @@ -21,3 +22,16 @@ export const toSinglePropertyDiagnosticsModel = (eventName, propertyTitle, prope const metadata = { [propertyTitle]: property }; return toDiagnosticsModel(eventName, metadata); } + +export const toDeviceDiagnosticsModel = (eventName, deviceFormData) => +{ + const metadata = { + DeviceIDType: deviceFormData.isSimulated ? '' : (deviceFormData.isGenerateId ? 'Generated' : 'Manual'), + DeviceType: deviceFormData.isSimulated ? Config.deviceType.simulated : Config.deviceType.physical, + NumberOfDevices: deviceFormData.count, + DeviceModel: deviceFormData.isSimulated ? deviceFormData.deviceModel : '', + AuthType: deviceFormData.isSimulated ? '' : (deviceFormData.authenticationType ? 'x.509' : 'Symmetric Key'), + AuthKey: deviceFormData.isSimulated ? '' : (deviceFormData.isGenerateKeys ? 'Auto': 'Manual') + } + return toDiagnosticsModel(eventName, metadata); +} diff --git a/src/services/telemetryService.js b/src/services/telemetryService.js index 2c625e597..d0fca62ec 100644 --- a/src/services/telemetryService.js +++ b/src/services/telemetryService.js @@ -2,7 +2,7 @@ import { stringify } from 'query-string'; import Config from 'app.config'; -import { HttpClient } from './httpClient'; +import { HttpClient } from 'utilities/httpClient'; import { toActiveAlertsModel, toAlertForRuleModel, diff --git a/src/store/configureStore.js b/src/store/configureStore.js index 2fb289b44..0986e8b81 100644 --- a/src/store/configureStore.js +++ b/src/store/configureStore.js @@ -6,7 +6,7 @@ import { composeWithDevTools } from 'redux-devtools-extension'; import rootEpic from './rootEpic'; import rootReducer from './rootReducer'; -export default function configureStore() { +export function configureStore() { // Initialize the redux-observable epics const epicMiddleware = createEpicMiddleware(rootEpic); // Initialize the redux store with middleware diff --git a/src/store/reducers/appReducer.js b/src/store/reducers/appReducer.js index d8588ffec..2f6c4e9cc 100644 --- a/src/store/reducers/appReducer.js +++ b/src/store/reducers/appReducer.js @@ -20,6 +20,7 @@ import { getError } from 'store/utilities'; import { svgs, compareByProperty } from 'utilities'; +import { toDiagnosticsModel } from 'services/models'; // ========================= Epics - START const handleError = fromAction => error => @@ -85,6 +86,16 @@ export const epics = createEpicScenario({ description: currSettings.description, diagnosticsOptIn: fromAction.payload }; + + var logEventName = 'Diagnostics_OptIn'; + if (!fromAction.payload) { + logEventName = 'Diagnostics_OptOut'; + } + var logPayload = toDiagnosticsModel(logEventName, {}); + logPayload.sessionId = getSessionId(store.getState()); + logPayload.eventProperties.CurrentWindow = getCurrentWindow(store.getState()); + DiagnosticsService.logEvent(logPayload).subscribe(); + return ConfigService.updateSolutionSettings(settings) .map(toActionCreator(redux.actions.updateSolutionSettings, fromAction)) .catch(handleError(fromAction)); diff --git a/src/store/reducers/devicesReducer.js b/src/store/reducers/devicesReducer.js index 488b76449..e8ce1b8f3 100644 --- a/src/store/reducers/devicesReducer.js +++ b/src/store/reducers/devicesReducer.js @@ -82,9 +82,15 @@ const deleteDevicesReducer = (state, { payload }) => { const insertDevicesReducer = (state, { payload }) => { const { entities: { devices }, result } = normalize(payload, deviceListSchema); + if (state.entities) { + return update(state, { + entities: { $merge: devices }, + items: { $splice: [[0, 0, result]] } + }); + } return update(state, { - entities: { $merge: devices }, - items: { $push: result } + entities: { $set: devices }, + items: { $set: [result] } }); }; diff --git a/src/store/reducers/rulesReducer.js b/src/store/reducers/rulesReducer.js index 7348af015..198963572 100644 --- a/src/store/reducers/rulesReducer.js +++ b/src/store/reducers/rulesReducer.js @@ -94,9 +94,15 @@ const initialState = { ...errorPendingInitialState, entities: {}, items: [] }; const insertRulesReducer = (state, { payload }) => { const { entities: { rules }, result } = normalize(payload, ruleListSchema); + if (state.entities) { + return update(state, { + entities: { $merge: rules }, + items: { $splice: [[0, 0, result]] } + }); + } return update(state, { - entities: { $merge: rules }, - items: { $splice: [[state.items.length, 0, result]] } + entities: { $set: rules }, + items: { $set: [result] } }); }; diff --git a/src/store/rootEpic.js b/src/store/rootEpic.js index 12e553d7f..d947da39b 100644 --- a/src/store/rootEpic.js +++ b/src/store/rootEpic.js @@ -3,7 +3,6 @@ import { combineEpics } from 'redux-observable'; // Epics -import { epics as exampleEpics } from './reducers/_exampleReducer'; import { epics as appEpics } from './reducers/appReducer'; import { epics as devicesEpics } from './reducers/devicesReducer'; import { epics as rulesEpics } from './reducers/rulesReducer'; @@ -12,7 +11,6 @@ import { epics as simulationEpics } from './reducers/deviceSimulationReducer'; // Extract the epic function from each property object const epics = [ - ...exampleEpics.getEpics(), ...appEpics.getEpics(), ...devicesEpics.getEpics(), ...packagesEpics.getEpics(), diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js index 746e5c0e8..70ce522e9 100644 --- a/src/store/rootReducer.js +++ b/src/store/rootReducer.js @@ -3,7 +3,6 @@ import { combineReducers } from 'redux'; // Reducers -import { reducer as exampleReducer } from './reducers/_exampleReducer'; import { reducer as appReducer } from './reducers/appReducer'; import { reducer as simulationReducer } from './reducers/deviceSimulationReducer'; import { reducer as devicesReducer } from './reducers/devicesReducer'; @@ -11,7 +10,6 @@ import { reducer as rulesReducer } from './reducers/rulesReducer'; import { reducer as packagesReducer } from './reducers/packagesReducer'; const rootReducer = combineReducers({ - ...exampleReducer, ...appReducer, ...devicesReducer, ...packagesReducer, diff --git a/src/services/models/ajaxModels.js b/src/utilities/ajaxModels.js similarity index 100% rename from src/services/models/ajaxModels.js rename to src/utilities/ajaxModels.js diff --git a/src/services/httpClient.js b/src/utilities/httpClient.js similarity index 97% rename from src/services/httpClient.js rename to src/utilities/httpClient.js index 142d370b8..a5b2eb130 100644 --- a/src/services/httpClient.js +++ b/src/utilities/httpClient.js @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. import { Observable } from 'rxjs'; -import { AuthService } from './authService'; +import { AuthService } from 'services/authService'; import Config from 'app.config'; -import { AjaxError, RetryableAjaxError } from './models'; +import { AjaxError, RetryableAjaxError } from './ajaxModels'; /** * A class of static methods for creating ajax requests diff --git a/src/services/httpClient.test.js b/src/utilities/httpClient.test.js similarity index 89% rename from src/services/httpClient.test.js rename to src/utilities/httpClient.test.js index 2c66384fb..25d785205 100644 --- a/src/services/httpClient.test.js +++ b/src/utilities/httpClient.test.js @@ -2,7 +2,7 @@ import 'polyfills'; import Config from 'app.config'; -import { HttpClient } from './httpClient'; +import { HttpClient } from 'utilities/httpClient'; const url = 'http://www.fakeurl.com'; diff --git a/src/utilities/index.js b/src/utilities/index.js index f066eee96..c002e6863 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -2,6 +2,8 @@ // Exports the app utilities +export * from './ajaxModels'; +export * from './httpClient'; export * from './methods'; export * from './svgs'; export * from './validation'; diff --git a/src/walkthrough/components/app.container.js b/src/walkthrough/components/app.container.js new file mode 100644 index 000000000..aff1c5938 --- /dev/null +++ b/src/walkthrough/components/app.container.js @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { AuthService } from 'services'; +import { + epics as appEpics, + getTheme, + getDeviceGroupFlyoutStatus +} from 'store/reducers/appReducer'; +import App from './app'; + +const mapStateToProps = state => ({ + theme: getTheme(state), + deviceGroupFlyoutIsOpen: getDeviceGroupFlyoutStatus(state) +}); + +// Wrap with the router and wrap the dispatch method +const mapDispatchToProps = dispatch => ({ + registerRouteEvent: pathname => dispatch(appEpics.actions.detectRouteChange(pathname)), + logout: () => AuthService.logout() +}); + +export const AppContainer = withRouter(translate()(connect(mapStateToProps, mapDispatchToProps)(App))); diff --git a/src/walkthrough/components/app.js b/src/walkthrough/components/app.js new file mode 100644 index 000000000..135c63fb7 --- /dev/null +++ b/src/walkthrough/components/app.js @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; + +import { svgs } from 'utilities'; +import Shell from "components/shell/shell"; +import { DashboardContainer, BasicPageContainer, PageWithFlyoutContainer, PageWithGridContainer } from './pages'; + +class App extends Component { + openSettings = () => alert('There are no settings in the walkthrough.'); + + render() { + const pagesConfig = [ + { + to: '/dashboard', + exact: true, + svg: svgs.tabs.dashboard, + labelId: 'walkthrough.tabs.dashboard', + component: DashboardContainer + }, + { + to: '/basicpage', + exact: true, + svg: svgs.tabs.example, + labelId: 'walkthrough.tabs.basicPage', + component: BasicPageContainer + }, + { + to: '/pagewithflyout', + exact: true, + svg: svgs.tabs.example, + labelId: 'walkthrough.tabs.pageWithFlyout', + component: PageWithFlyoutContainer + }, + { + to: '/pagewithgrid', + exact: true, + svg: svgs.tabs.example, + labelId: 'walkthrough.tabs.pageWithGrid', + component: PageWithGridContainer + } + ]; + + const crumbsConfig = [ + { + path: '/dashboard', crumbs: [ + { to: '/dashboard', labelId: 'walkthrough.tabs.dashboard' } + ] + }, + { + path: '/basicpage', crumbs: [ + { to: '/basicpage', labelId: 'walkthrough.tabs.basicPage' } + ] + }, + { + path: '/pagewithflyout', crumbs: [ + { to: '/pagewithflyout', labelId: 'walkthrough.tabs.pageWithFlyout' } + ] + }, + { + path: '/pagewithgrid', crumbs: [ + { to: '/pagewithgrid', labelId: 'walkthrough.tabs.pageWithGrid' } + ] + } + ]; + + const shellProps = { + pagesConfig, + crumbsConfig, + openSettings: this.openSettings, + ...this.props + }; + + return ( + + ); + } +} + +export default App; diff --git a/src/walkthrough/components/pages/basicPage/basicPage.container.js b/src/walkthrough/components/pages/basicPage/basicPage.container.js new file mode 100644 index 000000000..92ace17b0 --- /dev/null +++ b/src/walkthrough/components/pages/basicPage/basicPage.container.js @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { translate } from 'react-i18next'; + +import { BasicPage } from './basicPage'; + +export const BasicPageContainer = translate()(BasicPage); diff --git a/src/components/pages/_example/example.js b/src/walkthrough/components/pages/basicPage/basicPage.js similarity index 56% rename from src/components/pages/_example/example.js rename to src/walkthrough/components/pages/basicPage/basicPage.js index 52ce83d99..618eaa534 100644 --- a/src/components/pages/_example/example.js +++ b/src/walkthrough/components/pages/basicPage/basicPage.js @@ -4,14 +4,14 @@ import React, { Component } from 'react'; import { PageContent } from 'components/shared'; -import './example.css'; +import './basicPage.css'; -export class Example extends Component { +export class BasicPage extends Component { render() { const { t } = this.props; return ( - - { t('examples.pagePlaceholder') } + + { t('walkthrough.basicPage.pagePlaceholder') } ); } diff --git a/src/components/pages/_example/example.scss b/src/walkthrough/components/pages/basicPage/basicPage.scss similarity index 71% rename from src/components/pages/_example/example.scss rename to src/walkthrough/components/pages/basicPage/basicPage.scss index 52791649f..3e87ddc7a 100644 --- a/src/components/pages/_example/example.scss +++ b/src/walkthrough/components/pages/basicPage/basicPage.scss @@ -4,4 +4,4 @@ @import 'src/styles/mixins'; @import 'src/styles/themes'; -.example-container { padding: $baseContentPadding; } +.basic-page-container { padding: $baseContentPadding; } diff --git a/src/components/pages/_example/example.test.js b/src/walkthrough/components/pages/basicPage/basicPage.test.js similarity index 70% rename from src/components/pages/_example/example.test.js rename to src/walkthrough/components/pages/basicPage/basicPage.test.js index 576ea26d6..56c418727 100644 --- a/src/components/pages/_example/example.test.js +++ b/src/walkthrough/components/pages/basicPage/basicPage.test.js @@ -4,9 +4,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import 'polyfills'; -import { Example } from './example'; +import { BasicPage } from './basicPage'; -describe('Example Component', () => { +describe('BasicPage Component', () => { it('Renders without crashing', () => { const fakeProps = { @@ -14,7 +14,7 @@ describe('Example Component', () => { }; const wrapper = shallow( - + ); }); }); diff --git a/src/walkthrough/components/pages/dashboard/dashboard.container.js b/src/walkthrough/components/pages/dashboard/dashboard.container.js new file mode 100644 index 000000000..1bbdbec18 --- /dev/null +++ b/src/walkthrough/components/pages/dashboard/dashboard.container.js @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; + +import { Dashboard } from './dashboard'; + +export const DashboardContainer = translate()(connect(null, null)(Dashboard)); diff --git a/src/walkthrough/components/pages/dashboard/dashboard.js b/src/walkthrough/components/pages/dashboard/dashboard.js new file mode 100644 index 000000000..04954d81f --- /dev/null +++ b/src/walkthrough/components/pages/dashboard/dashboard.js @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; + +import { Grid, Cell } from 'components/pages/dashboard/grid'; + +import { ExamplePanel } from './panels'; +import { ContextMenu, PageContent } from 'components/shared'; + +import './dashboard.css'; + +const initialState = {}; + +export class Dashboard extends Component { + + constructor(props) { + super(props); + + this.state = initialState; + } + + render() { + const { t } = this.props; + + return [ + + {/** Add context buttons here... as needed for your dashboard. In this example, there are none. */} + , + + + + + + + + + + + + + + + + + + + + + + + + + + ]; + } +} diff --git a/src/walkthrough/components/pages/dashboard/dashboard.scss b/src/walkthrough/components/pages/dashboard/dashboard.scss new file mode 100644 index 000000000..094839176 --- /dev/null +++ b/src/walkthrough/components/pages/dashboard/dashboard.scss @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.dashboard-container { + height: 100%; + display: flex; + flex-grow: 1; + flex-flow: column nowrap; + align-items: stretch; + + .grid-container { + padding: 10px; + + .grid-cell { + min-height: 400px; + + &.devices-overview-cell { min-width: 314px; } + } + } + + @include themify($themes) { + background-color: themed('colorContentBackground'); + color: themed('colorContentText'); + } +} + +@media (max-width: 1200px) { + .dashboard-container { height: auto; } +} diff --git a/src/walkthrough/components/pages/dashboard/dashboard.test.js b/src/walkthrough/components/pages/dashboard/dashboard.test.js new file mode 100644 index 000000000..9cf680ee5 --- /dev/null +++ b/src/walkthrough/components/pages/dashboard/dashboard.test.js @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; +import { shallow } from 'enzyme'; +import 'polyfills'; + +import { Dashboard } from './dashboard'; + +describe('Dashboard Component', () => { + it('Renders without crashing', () => { + + const fakeProps = { + t: () => {} + }; + + const wrapper = shallow( + + ); + }); +}); diff --git a/src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.js b/src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.js new file mode 100644 index 000000000..f61f3661e --- /dev/null +++ b/src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.js @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; + +import { + Panel, + PanelHeader, + PanelHeaderLabel, + PanelContent, +} from 'components/pages/dashboard/panel'; + +import './examplePanel.css'; + +export class ExamplePanel extends Component { + constructor(props) { + super(props); + + this.state = { isPending: true }; + } + + render() { + const { t } = this.props; + + return ( + + + {t('walkthrough.dashboard.panels.example.header')} + + + {t('walkthrough.dashboard.panels.example.panelBody')} + + + ); + } +} diff --git a/src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.scss b/src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.scss new file mode 100644 index 000000000..e81169e5d --- /dev/null +++ b/src/walkthrough/components/pages/dashboard/panels/examplePanel/examplePanel.scss @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.example-panel-container { + display: flex; + flex-flow: column nowrap; + padding: 0 !important; + + @include themify($themes) { + color: themed('colorContentTextDim'); + } +} diff --git a/src/walkthrough/components/pages/dashboard/panels/examplePanel/index.js b/src/walkthrough/components/pages/dashboard/panels/examplePanel/index.js new file mode 100644 index 000000000..ef0305829 --- /dev/null +++ b/src/walkthrough/components/pages/dashboard/panels/examplePanel/index.js @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './examplePanel'; diff --git a/src/walkthrough/components/pages/dashboard/panels/index.js b/src/walkthrough/components/pages/dashboard/panels/index.js new file mode 100644 index 000000000..ef0305829 --- /dev/null +++ b/src/walkthrough/components/pages/dashboard/panels/index.js @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './examplePanel'; diff --git a/src/walkthrough/components/pages/index.js b/src/walkthrough/components/pages/index.js new file mode 100644 index 000000000..071c53bd1 --- /dev/null +++ b/src/walkthrough/components/pages/index.js @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Exports the app page components + +export * from './basicPage/basicPage.container'; +export * from './dashboard/dashboard.container'; +export * from './pageWithFlyout/pageWithFlyout.container'; +export * from './pageWithGrid/pageWithGrid.container'; diff --git a/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.container.js b/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.container.js similarity index 100% rename from src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.container.js rename to src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.container.js diff --git a/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.js b/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.js similarity index 76% rename from src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.js rename to src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.js index edbf5b80d..307312d88 100644 --- a/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.js +++ b/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; -import { ExampleService } from 'services'; +import { ExampleService } from 'walkthrough/services'; import { svgs } from 'utilities'; import { AjaxError, @@ -61,11 +61,11 @@ export class ExampleFlyout extends Component { const { isPending, changesApplied } = this.state; if (isPending) { - return t('examples.flyout.flyouts.example.pending'); + return t('walkthrough.pageWithFlyout.flyouts.example.pending'); } else if (changesApplied) { - return t('examples.flyout.flyouts.example.applySuccess'); + return t('walkthrough.pageWithFlyout.flyouts.example.applySuccess'); } else { - return t('examples.flyout.flyouts.example.affected'); + return t('walkthrough.pageWithFlyout.flyouts.example.affected'); } } @@ -86,7 +86,7 @@ export class ExampleFlyout extends Component { return ( - {t('examples.flyout.flyouts.example.header')} + {t('walkthrough.pageWithFlyout.flyouts.example.header')} @@ -97,14 +97,14 @@ export class ExampleFlyout extends Component { * */ }
-
{t('examples.flyout.flyouts.example.header')}
-
{t('examples.flyout.flyouts.example.description')}
+
{t('walkthrough.pageWithFlyout.flyouts.example.header')}
+
{t('walkthrough.pageWithFlyout.flyouts.example.description')}
-
{t('examples.flyout.flyouts.example.insertFormHere')}
+
{t('walkthrough.pageWithFlyout.flyouts.example.insertFormHere')}
{/** Sumarizes the action being taken; including count of items affected & status/pending indicator */} - {t('examples.flyout.flyouts.example.summaryHeader')} + {t('walkthrough.pageWithFlyout.flyouts.example.summaryHeader')} {summaryCount} {summaryMessage} @@ -119,8 +119,8 @@ export class ExampleFlyout extends Component { /** If changes are not yet applied, show the buttons for applying changes and closing the flyout. */ !changesApplied && - {t('examples.flyout.flyouts.example.apply')} - {t('examples.flyout.flyouts.example.cancel')} + {t('walkthrough.pageWithFlyout.flyouts.example.apply')} + {t('walkthrough.pageWithFlyout.flyouts.example.cancel')} } { @@ -131,7 +131,7 @@ export class ExampleFlyout extends Component { * */ !!changesApplied && - {t('examples.flyout.flyouts.example.close')} + {t('walkthrough.pageWithFlyout.flyouts.example.close')} } diff --git a/src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.scss b/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.scss similarity index 100% rename from src/components/pages/_flyoutExample/flyouts/exampleFlyout/exampleFlyout.scss rename to src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/exampleFlyout.scss diff --git a/src/components/pages/_flyoutExample/flyouts/exampleFlyout/index.js b/src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/index.js similarity index 100% rename from src/components/pages/_flyoutExample/flyouts/exampleFlyout/index.js rename to src/walkthrough/components/pages/pageWithFlyout/flyouts/exampleFlyout/index.js diff --git a/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.container.js b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.container.js new file mode 100644 index 000000000..21494b33e --- /dev/null +++ b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.container.js @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { translate } from 'react-i18next'; + +import { PageWithFlyout } from './pageWithFlyout'; + +export const PageWithFlyoutContainer = translate()(PageWithFlyout); diff --git a/src/components/pages/_flyoutExample/flyoutExample.js b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.js similarity index 78% rename from src/components/pages/_flyoutExample/flyoutExample.js rename to src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.js index a8e832901..77ec0ce8c 100644 --- a/src/components/pages/_flyoutExample/flyoutExample.js +++ b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.js @@ -6,11 +6,11 @@ import { Btn, ContextMenu, PageContent } from 'components/shared'; import { svgs } from 'utilities'; import { ExampleFlyoutContainer } from './flyouts/exampleFlyout'; -import './flyoutExample.css'; +import './pageWithFlyout.css'; const closedFlyoutState = { openFlyoutName: undefined }; -export class FlyoutExample extends Component { +export class PageWithFlyout extends Component { constructor(props) { super(props); this.state = closedFlyoutState; @@ -28,10 +28,10 @@ export class FlyoutExample extends Component { return [ - {t('examples.flyout.open')} + {t('walkthrough.pageWithFlyout.open')} , - - {t('examples.flyout.pageBody')} + + {t('walkthrough.pageWithFlyout.pageBody')} { isExampleFlyoutOpen && } ]; diff --git a/src/components/pages/_flyoutExample/flyoutExample.scss b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.scss similarity index 69% rename from src/components/pages/_flyoutExample/flyoutExample.scss rename to src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.scss index 046fcf1a9..0c6aa6b80 100644 --- a/src/components/pages/_flyoutExample/flyoutExample.scss +++ b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.scss @@ -4,4 +4,4 @@ @import 'src/styles/mixins'; @import 'src/styles/themes'; -.flyout-example-container { padding: $baseContentPadding; } +.page-with-flyout-container { padding: $baseContentPadding; } diff --git a/src/components/pages/_flyoutExample/flyoutExample.test.js b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.test.js similarity index 66% rename from src/components/pages/_flyoutExample/flyoutExample.test.js rename to src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.test.js index b6e17be17..7f6f2a0dc 100644 --- a/src/components/pages/_flyoutExample/flyoutExample.test.js +++ b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.test.js @@ -4,9 +4,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import 'polyfills'; -import { FlyoutExample } from './flyoutExample'; +import { PageWithFlyout } from './pageWithFlyout'; -describe('FlyoutExample Component', () => { +describe('PageWithFlyout Component', () => { it('Renders without crashing', () => { const fakeProps = { @@ -14,7 +14,7 @@ describe('FlyoutExample Component', () => { }; const wrapper = shallow( - + ); }); }); diff --git a/src/components/pages/_gridExample/exampleGrid/exampleGrid.js b/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js similarity index 96% rename from src/components/pages/_gridExample/exampleGrid/exampleGrid.js rename to src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js index f7ece34b7..622994efb 100644 --- a/src/components/pages/_gridExample/exampleGrid/exampleGrid.js +++ b/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js @@ -32,8 +32,8 @@ export class ExampleGrid extends Component { // Set up the available context buttons. // If these are subject to user permissions, use the Protected component (src/components/shared/protected). this.contextBtns = [ - {props.t('examples.grid.btn1')}, - {props.t('examples.grid.btn2')} + {props.t('walkthrough.pageWithGrid.grid.btn1')}, + {props.t('walkthrough.pageWithGrid.grid.btn2')} ]; } diff --git a/src/components/pages/_gridExample/exampleGrid/exampleGridConfig.js b/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGridConfig.js similarity index 86% rename from src/components/pages/_gridExample/exampleGrid/exampleGridConfig.js rename to src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGridConfig.js index 044e4e5c3..440ef2433 100644 --- a/src/components/pages/_gridExample/exampleGrid/exampleGridConfig.js +++ b/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGridConfig.js @@ -6,13 +6,13 @@ import { SoftSelectLinkRenderer } from 'components/shared/cellRenderers'; /** A collection of column definitions for the example grid */ export const exampleColumnDefs = { id: { - headerName: 'examples.grid.name', + headerName: 'walkthrough.pageWithGrid.grid.name', field: 'id', sort: 'asc', cellRendererFramework: SoftSelectLinkRenderer }, description: { - headerName: 'examples.grid.description', + headerName: 'walkthrough.pageWithGrid.grid.description', field: 'descr' } }; diff --git a/src/components/pages/_gridExample/exampleGrid/index.js b/src/walkthrough/components/pages/pageWithGrid/exampleGrid/index.js similarity index 100% rename from src/components/pages/_gridExample/exampleGrid/index.js rename to src/walkthrough/components/pages/pageWithGrid/exampleGrid/index.js diff --git a/src/components/pages/_gridExample/gridExample.container.js b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.container.js similarity index 74% rename from src/components/pages/_gridExample/gridExample.container.js rename to src/walkthrough/components/pages/pageWithGrid/pageWithGrid.container.js index feefb7d7b..d3a40c0da 100644 --- a/src/components/pages/_gridExample/gridExample.container.js +++ b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.container.js @@ -9,8 +9,8 @@ import { getExamplesError, getExamplesLastUpdated, getExamplesPendingStatus -} from 'store/reducers/_exampleReducer'; -import { GridExample } from './gridExample'; +} from 'walkthrough/store/reducers/exampleReducer'; +import { PageWithGrid } from './pageWithGrid'; // Pass the data const mapStateToProps = state => ({ @@ -25,4 +25,4 @@ const mapDispatchToProps = dispatch => ({ fetchData: () => dispatch(exampleEpics.actions.fetchExamples()) }); -export const GridExampleContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(GridExample)); +export const PageWithGridContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(PageWithGrid)); diff --git a/src/components/pages/_gridExample/gridExample.js b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.js similarity index 89% rename from src/components/pages/_gridExample/gridExample.js rename to src/walkthrough/components/pages/pageWithGrid/pageWithGrid.js index 4a8c839c9..62cf2b4d8 100644 --- a/src/components/pages/_gridExample/gridExample.js +++ b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.js @@ -10,9 +10,9 @@ import { } from 'components/shared'; import { ExampleGrid } from './exampleGrid'; -import './gridExample.css'; +import './pageWithGrid.css'; -export class GridExample extends Component { +export class PageWithGrid extends Component { constructor(props) { super(props); this.state = { contextBtns: null }; @@ -40,7 +40,7 @@ export class GridExample extends Component { {this.state.contextBtns} , - + {!!error && } {!error && } diff --git a/src/components/pages/_gridExample/gridExample.scss b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.scss similarity index 70% rename from src/components/pages/_gridExample/gridExample.scss rename to src/walkthrough/components/pages/pageWithGrid/pageWithGrid.scss index 0c893380c..6cea2ea32 100644 --- a/src/components/pages/_gridExample/gridExample.scss +++ b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.scss @@ -4,4 +4,4 @@ @import 'src/styles/mixins'; @import 'src/styles/themes'; -.grid-example-container { padding: $baseContentPadding; } +.page-with-grid-container { padding: $baseContentPadding; } diff --git a/src/components/pages/_gridExample/gridExample.test.js b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.test.js similarity index 75% rename from src/components/pages/_gridExample/gridExample.test.js rename to src/walkthrough/components/pages/pageWithGrid/pageWithGrid.test.js index b4ae1b882..631e59f41 100644 --- a/src/components/pages/_gridExample/gridExample.test.js +++ b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.test.js @@ -4,9 +4,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import 'polyfills'; -import { GridExample } from './gridExample'; +import { PageWithGrid } from './pageWithGrid'; -describe('GridExample Component', () => { +describe('PageWithGrid Component', () => { it('Renders without crashing', () => { const fakeProps = { @@ -19,7 +19,7 @@ describe('GridExample Component', () => { }; const wrapper = shallow( - + ); }); }); diff --git a/src/services/_exampleService.js b/src/walkthrough/services/exampleService.js similarity index 100% rename from src/services/_exampleService.js rename to src/walkthrough/services/exampleService.js diff --git a/src/walkthrough/services/index.js b/src/walkthrough/services/index.js new file mode 100644 index 000000000..1eed9f524 --- /dev/null +++ b/src/walkthrough/services/index.js @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Exports the shared react components into as a library + +export * from './exampleService'; diff --git a/src/services/models/_exampleModels.js b/src/walkthrough/services/models/exampleModels.js similarity index 100% rename from src/services/models/_exampleModels.js rename to src/walkthrough/services/models/exampleModels.js diff --git a/src/walkthrough/services/models/index.js b/src/walkthrough/services/models/index.js new file mode 100644 index 000000000..5e7ed9a22 --- /dev/null +++ b/src/walkthrough/services/models/index.js @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Exports models + +export * from './exampleModels'; diff --git a/src/walkthrough/store/configureStore.js b/src/walkthrough/store/configureStore.js new file mode 100644 index 000000000..0986e8b81 --- /dev/null +++ b/src/walkthrough/store/configureStore.js @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createStore, applyMiddleware } from 'redux'; +import { createEpicMiddleware } from 'redux-observable'; +import { composeWithDevTools } from 'redux-devtools-extension'; +import rootEpic from './rootEpic'; +import rootReducer from './rootReducer'; + +export function configureStore() { + // Initialize the redux-observable epics + const epicMiddleware = createEpicMiddleware(rootEpic); + // Initialize the redux store with middleware + return createStore( + rootReducer, + composeWithDevTools( + applyMiddleware(epicMiddleware) + ) + ); +} diff --git a/src/store/reducers/_exampleReducer.js b/src/walkthrough/store/reducers/exampleReducer.js similarity index 98% rename from src/store/reducers/_exampleReducer.js rename to src/walkthrough/store/reducers/exampleReducer.js index 8ae37a311..00f3b9af5 100644 --- a/src/store/reducers/_exampleReducer.js +++ b/src/walkthrough/store/reducers/exampleReducer.js @@ -6,7 +6,7 @@ import moment from 'moment'; import { schema, normalize } from 'normalizr'; import update from 'immutability-helper'; import { createSelector } from 'reselect'; -import { ExampleService } from 'services'; +import { ExampleService } from 'walkthrough/services'; import { createReducerScenario, createEpicScenario, diff --git a/src/walkthrough/store/rootEpic.js b/src/walkthrough/store/rootEpic.js new file mode 100644 index 000000000..e85807ebb --- /dev/null +++ b/src/walkthrough/store/rootEpic.js @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { combineEpics } from 'redux-observable'; + +// Epics +import { epics as appEpics } from 'store/reducers/appReducer'; +import { epics as deviceSimulationEpics } from 'store/reducers/deviceSimulationReducer'; +import { epics as exampleEpics } from './reducers/exampleReducer'; + +// Extract the epic function from each property object +const epics = [ + ...appEpics.getEpics(), + ...deviceSimulationEpics.getEpics(), + ...exampleEpics.getEpics() +]; + +const rootEpic = combineEpics(...epics); + +export default rootEpic; diff --git a/src/walkthrough/store/rootReducer.js b/src/walkthrough/store/rootReducer.js new file mode 100644 index 000000000..3e1f0feb5 --- /dev/null +++ b/src/walkthrough/store/rootReducer.js @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { combineReducers } from 'redux'; + +// Reducers +import { reducer as appReducer } from 'store/reducers/appReducer'; +import { reducer as deviceSimulationReducer } from 'store/reducers/deviceSimulationReducer'; +import { reducer as exampleReducer } from './reducers/exampleReducer'; + +const rootReducer = combineReducers({ + ...appReducer, + ...deviceSimulationReducer, + ...exampleReducer +}); + +export default rootReducer; From 8a729a5fb8e44dbddeb63f005308dbf02f0a6aef Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 6 Sep 2018 16:26:54 -0700 Subject: [PATCH 07/25] Deployments page scaffolding (#1088) --- public/locales/en/translations.json | 6 +++++- .../{deployments.svg => tabDeployments.svg} | 0 src/components/app.js | 21 ++++++++++++++++++- .../deployments/deployments.container.js | 9 ++++++++ .../pages/deployments/deployments.js | 20 ++++++++++++++++++ .../pages/deployments/deployments.scss | 9 ++++++++ .../pages/deployments/deployments.test.js | 20 ++++++++++++++++++ src/components/pages/index.js | 1 + src/utilities/svgs.js | 2 ++ 9 files changed, 86 insertions(+), 2 deletions(-) rename src/assets/icons/{deployments.svg => tabDeployments.svg} (100%) create mode 100644 src/components/pages/deployments/deployments.container.js create mode 100644 src/components/pages/deployments/deployments.js create mode 100644 src/components/pages/deployments/deployments.scss create mode 100644 src/components/pages/deployments/deployments.test.js diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 93d630fbf..359030584 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -17,7 +17,8 @@ "devices": "Devices", "rules": "Rules", "maintenance": "Maintenance", - "packages": "Packages" + "packages": "Packages", + "deployments": "Deployments" }, "errorFormat": "Error: {{message}}", "errorCode": { @@ -580,6 +581,9 @@ "edgemanifest": "Edge Manifest" } }, + "deployments": { + "header": "Deployments" + }, "walkthrough": { "tabs": { "dashboard": "Dashboard", diff --git a/src/assets/icons/deployments.svg b/src/assets/icons/tabDeployments.svg similarity index 100% rename from src/assets/icons/deployments.svg rename to src/assets/icons/tabDeployments.svg diff --git a/src/components/app.js b/src/components/app.js index 7f46ce9d4..3dab1e33e 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -5,7 +5,14 @@ import React, { Component } from 'react'; import { svgs } from 'utilities'; import Shell from "components/shell/shell"; import { ManageDeviceGroupsContainer, SettingsContainer } from 'components/shell/flyouts'; -import { DashboardContainer, DevicesContainer, RulesContainer, MaintenanceContainer, PackagesContainer } from './pages'; +import { + DashboardContainer, + DevicesContainer, + RulesContainer, + MaintenanceContainer, + PackagesContainer, + DeploymentsContainer +} from './pages'; class App extends Component { @@ -52,6 +59,13 @@ class App extends Component { labelId: 'tabs.packages', component: PackagesContainer }, + { + to: '/deployments', + exact: true, + svg: svgs.tabs.deployments, + labelId: 'tabs.deployments', + component: DeploymentsContainer + }, { to: '/maintenance', exact: false, @@ -82,6 +96,11 @@ class App extends Component { { to: '/packages', labelId: 'tabs.packages' } ] }, + { + path: '/deployments', crumbs: [ + { to: '/deployments', labelId: 'tabs.deployments' } + ] + }, { path: '/maintenance', crumbs: [ { to: '/maintenance', labelId: 'tabs.maintenance' } diff --git a/src/components/pages/deployments/deployments.container.js b/src/components/pages/deployments/deployments.container.js new file mode 100644 index 000000000..a75ff5db2 --- /dev/null +++ b/src/components/pages/deployments/deployments.container.js @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { translate } from 'react-i18next'; + +import { Deployments } from './deployments'; + +// TODO + +export const DeploymentsContainer = translate()(Deployments); diff --git a/src/components/pages/deployments/deployments.js b/src/components/pages/deployments/deployments.js new file mode 100644 index 000000000..0b605cb0c --- /dev/null +++ b/src/components/pages/deployments/deployments.js @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; + +import { PageContent } from 'components/shared'; + +import './deployments.css'; + +export class Deployments extends Component { + // TODO + + render() { + const { t } = this.props; + return ( + + {t('deployments.header')} + + ); + } +} diff --git a/src/components/pages/deployments/deployments.scss b/src/components/pages/deployments/deployments.scss new file mode 100644 index 000000000..a5debe8d8 --- /dev/null +++ b/src/components/pages/deployments/deployments.scss @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/variables'; +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.deployments-page-container { + // TODO +} diff --git a/src/components/pages/deployments/deployments.test.js b/src/components/pages/deployments/deployments.test.js new file mode 100644 index 000000000..15cb79866 --- /dev/null +++ b/src/components/pages/deployments/deployments.test.js @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; +import { shallow } from 'enzyme'; +import 'polyfills'; + +import { Deployments } from './deployments'; + +describe('Deployments Component', () => { + it('Renders without crashing', () => { + + const fakeProps = { + t: () => {}, + }; + + const wrapper = shallow( + + ); + }); +}); diff --git a/src/components/pages/index.js b/src/components/pages/index.js index f41810530..693c84340 100644 --- a/src/components/pages/index.js +++ b/src/components/pages/index.js @@ -6,3 +6,4 @@ export * from './devices/devices.container'; export * from './rules/rules.container'; export * from './maintenance/maintenance.container'; export * from './packages/packages.container'; +export * from './deployments/deployments.container'; diff --git a/src/utilities/svgs.js b/src/utilities/svgs.js index b1b62c28e..7b4ed52f7 100644 --- a/src/utilities/svgs.js +++ b/src/utilities/svgs.js @@ -47,6 +47,7 @@ import SettingsIconPath from 'assets/icons/settings.svg'; import SimulatedDeviceIconPath from 'assets/icons/simulatedDevice.svg'; import TabDashboardIconPath from 'assets/icons/tabDashboard.svg'; import TabDevicesIconPath from 'assets/icons/tabDevices.svg'; +import TabDeploymentsIconPath from 'assets/icons/tabDeployments.svg'; import TabMaintenanceIconPath from 'assets/icons/tabMaintenance.svg'; import TabRulesIconPath from 'assets/icons/tabRules.svg'; import TrashIconPath from 'assets/icons/trash.svg'; @@ -59,6 +60,7 @@ export const svgs = { tabs: { dashboard: TabDashboardIconPath, devices: TabDevicesIconPath, + deployments: TabDeploymentsIconPath, maintenance: TabMaintenanceIconPath, packages: TabPackagesIconPath, rules: TabRulesIconPath, From 831ca5190e93f82553aca20a66ee4c4dbbc04075 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 6 Sep 2018 17:27:19 -0700 Subject: [PATCH 08/25] Merge Master into Edge Feature Branch to keep it up to date (#1091) * Dev Walkthru: add a new Panel to the Dashboard (#1062) * Dev Walkthru: add a new Panel to the Dashboard * small tweaks, review feedback * fix bad code end marker * Diagnostics bugFix (#1065) * flatMap * Dummy comment to retrigger build * Add Rule Diagnostics (#1064) Add diagnostics logging for rule create/update events. Added the following metrics: Rule_NewClick Rule_EditClick Rule_DeviceGroupClick Rule_CalculationClick Rule_FieldClick Rule_OperatorClick Rule_AddConditionClick Rule_SeverityLevelClick Rule_StatusToggle Rule_ApplyClick Rule_CancelClick Rule_TopXCloseClick Also includes new "sessionid" sections of diagnostics call, which logs the time in ms since Jan 1, 1970 when the page was loaded (amplitude expects session id in this format). This fields will be added by diagnostics to enable logging of session id to amplitude--until those changes go in it will be ignored by the backend. * Delete .travis.yml (#1066) * Delete .travis.yml * Update README.md * Add diagnostics for new device funnel (#1075) * Add device metrics Add metrics for new device flyout * Add device created metric Add metric on device create so we can log device id for physical devices * Fix rule apply event Rule apply click event was only emitted for new rules. Move call so it is emitted if a rule is added or edited. * Address comments and align metric names Address comments. Update metric names to be in same format as rule metrics * fix insertion when entities are null for devices and rules (#1078) * Refactor to make walkthrough code less intrusive (#1069) * refactor to make walkthrough code less intrusive * refactor after meeting with team * update MD files, move httpClient * review feedback * fix nit * update breadcrumbs to use isDef instead of checking undefined * Treat text and number correctly in device jobs (#1082) * treat text and number correctly in device jobs * fix formatting nit * Rule updates need to send ETag (#1084) UI needs to send the ETag when updating rules. * Rule enable/disable needs to update ETag in redux store (#1086) * Small updates for the add page walkthrough (#1089) * Making cloudToDeviceMethod to empty an string (#1090) --- docs/walkthrough/addNewPage.md | 6 +++--- .../flyouts/deviceJobs/deviceJobProperties.js | 2 +- .../devices/flyouts/deviceJobs/deviceJobTags.js | 2 +- .../pages/rules/flyouts/ruleDelete/deleteRule.js | 4 ++-- .../pages/rules/flyouts/ruleEditor/ruleEditor.js | 6 +++--- .../pages/rules/flyouts/ruleStatus/ruleStatus.js | 10 +++++----- src/components/shell/header/breadcrumbs.js | 4 ++-- src/services/models/iotHubManagerModels.js | 15 ++++++++++----- src/services/models/telemetryModels.js | 8 ++++++-- src/utilities/methods.js | 3 +++ 10 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/walkthrough/addNewPage.md b/docs/walkthrough/addNewPage.md index a219226fd..c3a9f8053 100644 --- a/docs/walkthrough/addNewPage.md +++ b/docs/walkthrough/addNewPage.md @@ -13,15 +13,15 @@ The following is for creating a new page called "**basicPage**." ```js export * from './basicPage/basicPage.container'; ``` -1. (Optional) Add an SVG icon for the new page. See [utilities/README.md](../utilities/README.md) for more information. +1. (Optional) Add an SVG icon for the new page. See [utilities/README.md](/src/utilities/README.md) for more information. - Note that existing SVGs can be used as well. 1. Add the page name to the translations file, [translations.json](../../public/locales/en/translations.json). [i18next][i18next] is used for internationalization. ```json "tabs": { - "example": "Example", + "template": "Example", }, ``` -1. Open the top level application page, [walkthrough/components/app.js](/src/components/app.js). +1. Open the top level application page. For the walkthrough sample code: [walkthrough/components/app.js](/src/walkthrough/components/app.js). For the normal applicaiton: [components/app.js](/src/components/app.js) 1. Add the new page to the imports. ```javascript // Page Components diff --git a/src/components/pages/devices/flyouts/deviceJobs/deviceJobProperties.js b/src/components/pages/devices/flyouts/deviceJobs/deviceJobProperties.js index faa00ed79..869549cad 100644 --- a/src/components/pages/devices/flyouts/deviceJobs/deviceJobProperties.js +++ b/src/components/pages/devices/flyouts/deviceJobs/deviceJobProperties.js @@ -33,7 +33,7 @@ import { update.extend('$autoArray', (val, obj) => update(obj || [], val)); -const isNumeric = value => typeof value === 'number' || !isNaN(parseInt(value, 10)); +const isNumeric = value => typeof value === 'number'; const isAlphaNumericRegex = /^[a-zA-Z0-9]*$/; const nonAlphaNumeric = x => !x.match(isAlphaNumericRegex); diff --git a/src/components/pages/devices/flyouts/deviceJobs/deviceJobTags.js b/src/components/pages/devices/flyouts/deviceJobs/deviceJobTags.js index a8f86b569..c546cc0e6 100644 --- a/src/components/pages/devices/flyouts/deviceJobs/deviceJobTags.js +++ b/src/components/pages/devices/flyouts/deviceJobs/deviceJobTags.js @@ -34,7 +34,7 @@ import { update.extend('$autoArray', (val, obj) => update(obj || [], val)); -const isNumeric = value => typeof value === 'number' || !isNaN(parseInt(value, 10)); +const isNumeric = value => typeof value === 'number'; const isAlphaNumericRegex = /^[a-zA-Z0-9]*$/; const nonAlphaNumeric = x => !x.match(isAlphaNumericRegex); diff --git a/src/components/pages/rules/flyouts/ruleDelete/deleteRule.js b/src/components/pages/rules/flyouts/ruleDelete/deleteRule.js index af2de6491..22de6bb73 100644 --- a/src/components/pages/rules/flyouts/ruleDelete/deleteRule.js +++ b/src/components/pages/rules/flyouts/ruleDelete/deleteRule.js @@ -14,7 +14,7 @@ import { } from 'components/shared'; import { svgs } from 'utilities'; import { TelemetryService } from 'services'; -import { toNewRuleRequestModel } from 'services/models'; +import { toEditRuleRequestModel } from 'services/models'; import Flyout from 'components/shared/flyout'; import { RuleSummary } from '..'; @@ -85,7 +85,7 @@ export class DeleteRule extends Component { this.setState({ isPending: true, error: null }); rule.enabled = status; this.subscription = - TelemetryService.updateRule(rule.id, toNewRuleRequestModel(rule)) + TelemetryService.updateRule(rule.id, toEditRuleRequestModel(rule)) .subscribe( (updatedRule) => { this.props.refresh(); diff --git a/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js b/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js index 7ef05576e..404ab0628 100644 --- a/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js +++ b/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js @@ -28,7 +28,7 @@ import { import Flyout from 'components/shared/flyout'; import { IoTHubManagerService, TelemetryService } from 'services'; import { - toNewRuleRequestModel, + toEditRuleRequestModel, ruleCalculations, ruleTimePeriods, ruleOperators, @@ -149,7 +149,7 @@ export class RuleEditor extends LinkedComponent { }; logEvent(toRuleDiagnosticsModel('Rule_ApplyClick', requestProps)); if (this.props.rule) { // If rule object exist then update the existing rule - this.subscription = TelemetryService.updateRule(this.props.rule.id, toNewRuleRequestModel(requestProps)) + this.subscription = TelemetryService.updateRule(this.props.rule.id, toEditRuleRequestModel(requestProps)) .subscribe( (updatedRule) => { modifyRules([{ ...updatedRule, ...countProps }]); @@ -158,7 +158,7 @@ export class RuleEditor extends LinkedComponent { error => this.setState({ error, isPending: false, changesApplied: true }) ); } else { // If rule object doesn't exist then create a new rule - this.subscription = TelemetryService.createRule(toNewRuleRequestModel(requestProps)) + this.subscription = TelemetryService.createRule(toEditRuleRequestModel(requestProps)) .subscribe( (createdRule) => { insertRules([{ ...createdRule, ...countProps }]); diff --git a/src/components/pages/rules/flyouts/ruleStatus/ruleStatus.js b/src/components/pages/rules/flyouts/ruleStatus/ruleStatus.js index 7e45c1d86..23c7a06ae 100644 --- a/src/components/pages/rules/flyouts/ruleStatus/ruleStatus.js +++ b/src/components/pages/rules/flyouts/ruleStatus/ruleStatus.js @@ -12,7 +12,7 @@ import { } from 'components/shared'; import { svgs } from 'utilities'; import { TelemetryService } from 'services'; -import { permissions, toNewRuleRequestModel } from 'services/models'; +import { permissions, toEditRuleRequestModel } from 'services/models'; import Flyout from 'components/shared/flyout'; import './ruleStatus.css'; @@ -60,8 +60,8 @@ export class RuleStatus extends Component { })); this.subscription = Observable.from(requestPropList) .flatMap((rule) => - TelemetryService.updateRule(rule.id, toNewRuleRequestModel(rule)) - .map(() => rule) + TelemetryService.updateRule(rule.id, toEditRuleRequestModel(rule)) + .map(updatedRule => ({...rule, eTag: updatedRule.eTag})) ) .subscribe( updatedRule => { @@ -95,8 +95,8 @@ export class RuleStatus extends Component {
{ - rules.map((rule) => ( - + rules.map((rule, idx) => ( + )) } diff --git a/src/components/shell/header/breadcrumbs.js b/src/components/shell/header/breadcrumbs.js index 9dde2334d..c45f7c034 100644 --- a/src/components/shell/header/breadcrumbs.js +++ b/src/components/shell/header/breadcrumbs.js @@ -28,8 +28,8 @@ export const Breadcrumbs = ({ t, crumbsConfig }) => ( { crumbsConfig.map(({ path, crumbs }) => - { - return crumbs.map((crumb, idx) => ); + { + return crumbs.map((crumb, idx) => ); }} /> ) } diff --git a/src/services/models/iotHubManagerModels.js b/src/services/models/iotHubManagerModels.js index 8c289abe1..b50e5259a 100644 --- a/src/services/models/iotHubManagerModels.js +++ b/src/services/models/iotHubManagerModels.js @@ -2,7 +2,7 @@ import update from 'immutability-helper'; import dot from 'dot-object'; -import { camelCaseReshape, getItems } from 'utilities'; +import { camelCaseReshape, getItems, float } from 'utilities'; import uuid from 'uuid/v4'; // Contains methods for converting service response @@ -64,7 +64,10 @@ export const toSubmitTagsJobRequestModel = (devices, { jobName, updatedTags, del const jobId = jobName ? jobName + '-' + uuid() : uuid(); const deviceIds = devices.map(({ id }) => `'${id}'`).join(','); const Tags = {}; - updatedTags.forEach(({ name, value }) => (Tags[name] = value)); + // Ensure type passed to server is correct as specified: number or text. + // The toString call is necessary when a number should be saved as text. + updatedTags.forEach(({ name, value, type }) => + (Tags[name] = type === 'Number' ? float(value) : value.toString())); deletedTags.forEach((name) => (Tags[name] = null)); const request = { JobId: jobId, @@ -81,7 +84,10 @@ export const toSubmitPropertiesJobRequestModel = (devices, { jobName, updatedPro const jobId = jobName ? jobName + '-' + uuid() : uuid(); const deviceIds = devices.map(({ id }) => `'${id}'`).join(','); const Desired = {}; - updatedProperties.forEach(({ name, value }) => (Desired[name] = value)); + // Ensure type passed to server is correct as specified: number or text. + // The toString call is necessary when a number should be saved as text. + updatedProperties.forEach(({ name, value, type }) => + (Desired[name] = type === 'Number' ? float(value) : value.toString())); deletedProperties.forEach((name) => (Desired[name] = null)); const request = { JobId: jobId, @@ -108,7 +114,7 @@ export const toSubmitMethodJobRequestModel = (devices, { jobName, methodName, fi Firmware: firmwareVersion, FirmwareUri: firmwareUri }) - : '{}'; + : ''; const request = { JobId: jobId, QueryCondition: `deviceId in [${deviceIds}]`, @@ -140,7 +146,6 @@ export const authenticationTypeOptions = { }; export const toNewDeviceRequestModel = ({ - count, deviceId, isGenerateId, isSimulated, diff --git a/src/services/models/telemetryModels.js b/src/services/models/telemetryModels.js index 55e48b43b..0c7bd795b 100644 --- a/src/services/models/telemetryModels.js +++ b/src/services/models/telemetryModels.js @@ -123,7 +123,8 @@ export const toMessagesModel = (response = {}) => getItems(response) 'time': 'time' })); -export const toNewRuleRequestModel = ({ +export const toEditRuleRequestModel = ({ + id, name, description, groupId, @@ -131,7 +132,8 @@ export const toNewRuleRequestModel = ({ severity, enabled, calculation, - timePeriod + timePeriod, + eTag }) => { const Conditions = conditions.map(condition => ({ Field: condition.field, @@ -139,6 +141,7 @@ export const toNewRuleRequestModel = ({ Value: condition.value })); return { + Id: id, Name: name, Description: description, GroupId: groupId, @@ -146,6 +149,7 @@ export const toNewRuleRequestModel = ({ Enabled: enabled, Calculation: calculation, TimePeriod: timePeriod, + ETag: eTag, Conditions }; } diff --git a/src/utilities/methods.js b/src/utilities/methods.js index 9da4d20c5..f252e516e 100644 --- a/src/utilities/methods.js +++ b/src/utilities/methods.js @@ -16,6 +16,9 @@ export const isEmptyObject = obj => Object.keys(obj).length === 0 && obj.constru /** Converts a value to an integer */ export const int = (num) => parseInt(num, 10); +/** Converts a value to a float */ +export const float = (num) => parseFloat(num, 10); + /** Merges css classnames into a single string */ export const joinClasses = (...classNames) => classNames.filter(name => !!name).join(' ').trim(); From e3cb97d7a11cdf6b7b1853ce8156430b18f3d38c Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 6 Sep 2018 22:10:19 -0700 Subject: [PATCH 09/25] Rename Package Flyouts to be consistent with rest of UI (#1087) * Rename Package Flyouts to be consistent with rest of UI * rename path --- src/components/pages/packages/flyouts/deletePackage/index.js | 4 ---- src/components/pages/packages/flyouts/index.js | 4 ++-- src/components/pages/packages/flyouts/newPackage/index.js | 4 ---- src/components/pages/packages/flyouts/packageDelete/index.js | 4 ++++ .../packageDelete.container.js} | 4 ++-- .../deletePackage.js => packageDelete/packageDelete.js} | 4 ++-- .../deletePackage.scss => packageDelete/packageDelete.scss} | 0 src/components/pages/packages/flyouts/packageNew/index.js | 4 ++++ .../packageNew.container.js} | 4 ++-- .../{newPackage/newPackage.js => packageNew/packageNew.js} | 4 ++-- .../newPackage.scss => packageNew/packageNew.scss} | 0 src/components/pages/packages/packages.js | 4 ++-- src/components/pages/packages/packagesGrid/packagesGrid.js | 4 ++-- 13 files changed, 22 insertions(+), 22 deletions(-) delete mode 100644 src/components/pages/packages/flyouts/deletePackage/index.js delete mode 100644 src/components/pages/packages/flyouts/newPackage/index.js create mode 100644 src/components/pages/packages/flyouts/packageDelete/index.js rename src/components/pages/packages/flyouts/{deletePackage/deletePackage.container.js => packageDelete/packageDelete.container.js} (78%) rename src/components/pages/packages/flyouts/{deletePackage/deletePackage.js => packageDelete/packageDelete.js} (95%) rename src/components/pages/packages/flyouts/{deletePackage/deletePackage.scss => packageDelete/packageDelete.scss} (100%) create mode 100644 src/components/pages/packages/flyouts/packageNew/index.js rename src/components/pages/packages/flyouts/{newPackage/newPackage.container.js => packageNew/packageNew.container.js} (80%) rename src/components/pages/packages/flyouts/{newPackage/newPackage.js => packageNew/packageNew.js} (98%) rename src/components/pages/packages/flyouts/{newPackage/newPackage.scss => packageNew/packageNew.scss} (100%) diff --git a/src/components/pages/packages/flyouts/deletePackage/index.js b/src/components/pages/packages/flyouts/deletePackage/index.js deleted file mode 100644 index 90a352d87..000000000 --- a/src/components/pages/packages/flyouts/deletePackage/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -export * from './deletePackage'; -export * from './deletePackage.container'; diff --git a/src/components/pages/packages/flyouts/index.js b/src/components/pages/packages/flyouts/index.js index 1602ebc7b..e8390b0d6 100644 --- a/src/components/pages/packages/flyouts/index.js +++ b/src/components/pages/packages/flyouts/index.js @@ -1,4 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -export * from './newPackage'; -export * from './deletePackage'; +export * from './packageNew'; +export * from './packageDelete'; diff --git a/src/components/pages/packages/flyouts/newPackage/index.js b/src/components/pages/packages/flyouts/newPackage/index.js deleted file mode 100644 index 0490c9475..000000000 --- a/src/components/pages/packages/flyouts/newPackage/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -export * from './newPackage'; -export * from './newPackage.container'; diff --git a/src/components/pages/packages/flyouts/packageDelete/index.js b/src/components/pages/packages/flyouts/packageDelete/index.js new file mode 100644 index 000000000..70e24381b --- /dev/null +++ b/src/components/pages/packages/flyouts/packageDelete/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './packageDelete'; +export * from './packageDelete.container'; diff --git a/src/components/pages/packages/flyouts/deletePackage/deletePackage.container.js b/src/components/pages/packages/flyouts/packageDelete/packageDelete.container.js similarity index 78% rename from src/components/pages/packages/flyouts/deletePackage/deletePackage.container.js rename to src/components/pages/packages/flyouts/packageDelete/packageDelete.container.js index 9ddeb9882..89376e544 100644 --- a/src/components/pages/packages/flyouts/deletePackage/deletePackage.container.js +++ b/src/components/pages/packages/flyouts/packageDelete/packageDelete.container.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { translate } from 'react-i18next'; -import { DeletePackage } from './deletePackage'; +import { PackageDelete } from './packageDelete'; import { getDeletePackageError, getDeletePackagePendingStatus, @@ -20,4 +20,4 @@ const mapDispatchToProps = dispatch => ({ deletePackage: packageId => dispatch(packagesEpics.actions.deletePackage(packageId)) }); -export const DeletePackageContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeletePackage)); +export const PackageDeleteContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(PackageDelete)); diff --git a/src/components/pages/packages/flyouts/deletePackage/deletePackage.js b/src/components/pages/packages/flyouts/packageDelete/packageDelete.js similarity index 95% rename from src/components/pages/packages/flyouts/deletePackage/deletePackage.js rename to src/components/pages/packages/flyouts/packageDelete/packageDelete.js index e13b3d0e5..3a814f243 100644 --- a/src/components/pages/packages/flyouts/deletePackage/deletePackage.js +++ b/src/components/pages/packages/flyouts/packageDelete/packageDelete.js @@ -12,9 +12,9 @@ import { } from 'components/shared'; import { svgs } from 'utilities'; -import './deletePackage.css'; +import './packageDelete.css'; -export class DeletePackage extends LinkedComponent { +export class PackageDelete extends LinkedComponent { constructor(props) { super(props); diff --git a/src/components/pages/packages/flyouts/deletePackage/deletePackage.scss b/src/components/pages/packages/flyouts/packageDelete/packageDelete.scss similarity index 100% rename from src/components/pages/packages/flyouts/deletePackage/deletePackage.scss rename to src/components/pages/packages/flyouts/packageDelete/packageDelete.scss diff --git a/src/components/pages/packages/flyouts/packageNew/index.js b/src/components/pages/packages/flyouts/packageNew/index.js new file mode 100644 index 000000000..0e1ae1514 --- /dev/null +++ b/src/components/pages/packages/flyouts/packageNew/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './packageNew'; +export * from './packageNew.container'; diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.container.js b/src/components/pages/packages/flyouts/packageNew/packageNew.container.js similarity index 80% rename from src/components/pages/packages/flyouts/newPackage/newPackage.container.js rename to src/components/pages/packages/flyouts/packageNew/packageNew.container.js index 7b8fa64c7..f39194171 100644 --- a/src/components/pages/packages/flyouts/newPackage/newPackage.container.js +++ b/src/components/pages/packages/flyouts/packageNew/packageNew.container.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { translate } from 'react-i18next'; -import { NewPackage } from './newPackage'; +import { PackageNew } from './packageNew'; import { getCreatePackageError, getCreatePackagePendingStatus, @@ -20,4 +20,4 @@ const mapDispatchToProps = dispatch => ({ createPackage: packageModel => dispatch(packagesEpics.actions.createPackage(packageModel)) }); -export const NewPackageContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(NewPackage)); +export const PackageNewContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(PackageNew)); diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.js b/src/components/pages/packages/flyouts/packageNew/packageNew.js similarity index 98% rename from src/components/pages/packages/flyouts/newPackage/newPackage.js rename to src/components/pages/packages/flyouts/packageNew/packageNew.js index 3cd54d8d7..227da6166 100644 --- a/src/components/pages/packages/flyouts/newPackage/newPackage.js +++ b/src/components/pages/packages/flyouts/packageNew/packageNew.js @@ -24,11 +24,11 @@ import { Svg } from 'components/shared'; -import './newPackage.css'; +import './packageNew.css'; const fileInputAccept = ".json,application/json"; -export class NewPackage extends LinkedComponent { +export class PackageNew extends LinkedComponent { constructor(props) { super(props); diff --git a/src/components/pages/packages/flyouts/newPackage/newPackage.scss b/src/components/pages/packages/flyouts/packageNew/packageNew.scss similarity index 100% rename from src/components/pages/packages/flyouts/newPackage/newPackage.scss rename to src/components/pages/packages/flyouts/packageNew/packageNew.scss diff --git a/src/components/pages/packages/packages.js b/src/components/pages/packages/packages.js index 5710a2cd4..6ba22de98 100644 --- a/src/components/pages/packages/packages.js +++ b/src/components/pages/packages/packages.js @@ -12,7 +12,7 @@ import { RefreshBar, PageTitle } from 'components/shared'; -import { NewPackageContainer } from './flyouts'; +import { PackageNewContainer } from './flyouts'; import { svgs } from 'utilities'; import './packages.css'; @@ -73,7 +73,7 @@ export class Packages extends Component { {!!error && } {!error && } - {this.state.openFlyoutName === 'new-Package' && } + {this.state.openFlyoutName === 'new-Package' && } ]; } diff --git a/src/components/pages/packages/packagesGrid/packagesGrid.js b/src/components/pages/packages/packagesGrid/packagesGrid.js index 56277ae66..2c7b60ffe 100644 --- a/src/components/pages/packages/packagesGrid/packagesGrid.js +++ b/src/components/pages/packages/packagesGrid/packagesGrid.js @@ -5,7 +5,7 @@ import { packagesColumnDefs, defaultPackagesGridProps } from './packagesGridConf import { Btn, PcsGrid, Protected } from 'components/shared'; import { isFunc, translateColumnDefs, svgs } from 'utilities'; import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig'; -import { DeletePackageContainer } from '../flyouts'; +import { PackageDeleteContainer } from '../flyouts'; import './packagesGrid.css'; @@ -39,7 +39,7 @@ export class PackagesGrid extends Component { getOpenFlyout = () => { if (this.state.openFlyoutName === 'delete-package') { - return + return } return null; } From eafa5dfca011161507d5f52a139f2186b9717a6c Mon Sep 17 00:00:00 2001 From: Mary Ellen Chaffin Date: Fri, 14 Sep 2018 11:01:59 -0700 Subject: [PATCH 10/25] Add glimmer icon to newly added grid items (#1095) * Add glimmer icon to newly added grid items --- public/locales/en/translations.json | 2 +- src/assets/icons/glimmer.svg | 1 + .../packagesGrid/packagesGridConfig.js | 6 ++-- .../pages/rules/rulesGrid/rulesGridConfig.js | 3 +- .../shared/cellRenderers/cellRenderer.scss | 28 ++++++++++++------- .../glimmerRenderer/glimmerRenderer.js | 14 ++++++++++ src/components/shared/cellRenderers/index.js | 1 + .../softSelectLinkRenderer.js | 14 +++++++--- src/store/reducers/devicesReducer.js | 3 +- src/store/reducers/packagesReducer.js | 3 +- src/store/reducers/rulesReducer.js | 5 ++-- src/styles/_themes.scss | 4 +++ src/utilities/svgs.js | 2 ++ 13 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 src/assets/icons/glimmer.svg create mode 100644 src/components/shared/cellRenderers/glimmerRenderer/glimmerRenderer.js diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 359030584..7157b1773 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -573,7 +573,7 @@ "delete": { "title": "Delete Package?", "delete": "Delete", - "cancel": "cancel", + "cancel": "Cancel", "info": "Deleting selected package will remove it. It will not impact any of the deployments of this package." } }, diff --git a/src/assets/icons/glimmer.svg b/src/assets/icons/glimmer.svg new file mode 100644 index 000000000..17549ac11 --- /dev/null +++ b/src/assets/icons/glimmer.svg @@ -0,0 +1 @@ + diff --git a/src/components/pages/packages/packagesGrid/packagesGridConfig.js b/src/components/pages/packages/packagesGrid/packagesGridConfig.js index 5fa62427f..41407a647 100644 --- a/src/components/pages/packages/packagesGrid/packagesGridConfig.js +++ b/src/components/pages/packages/packagesGrid/packagesGridConfig.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import Config from 'app.config'; -import { TimeRenderer } from 'components/shared/cellRenderers'; +import { TimeRenderer, SoftSelectLinkRenderer } from 'components/shared/cellRenderers'; import { gridValueFormatters } from 'components/shared/pcsGrid/pcsGridConfig'; const { checkForEmpty } = gridValueFormatters; @@ -10,7 +10,9 @@ export const packagesColumnDefs = { name: { headerName: 'packages.grid.name', field: 'name', - valueFormatter: ({ value }) => checkForEmpty(value) + sort: 'asc', + valueFormatter: ({ value }) => checkForEmpty(value), + cellRendererFramework: SoftSelectLinkRenderer }, type: { headerName: 'packages.grid.type', diff --git a/src/components/pages/rules/rulesGrid/rulesGridConfig.js b/src/components/pages/rules/rulesGrid/rulesGridConfig.js index 1539354ee..3bdd97db8 100644 --- a/src/components/pages/rules/rulesGrid/rulesGridConfig.js +++ b/src/components/pages/rules/rulesGrid/rulesGridConfig.js @@ -18,6 +18,7 @@ export const rulesColumnDefs = { ruleName: { headerName: 'rules.grid.ruleName', field: 'name', + sort: 'asc', filter: 'text', cellRendererFramework: SoftSelectLinkRenderer }, @@ -94,7 +95,7 @@ export const rulesColumnDefs = { headerName: 'rules.grid.explore', field: 'ruleId', cellRendererFramework: props => - }, + } }; export const defaultRulesGridProps = { diff --git a/src/components/shared/cellRenderers/cellRenderer.scss b/src/components/shared/cellRenderers/cellRenderer.scss index 76de2493a..59105e5a3 100644 --- a/src/components/shared/cellRenderers/cellRenderer.scss +++ b/src/components/shared/cellRenderers/cellRenderer.scss @@ -35,6 +35,22 @@ $iconSize: 14px; > .icon-only { display: none !important; } } + .soft-select-link-cell { + display: inline; + color: inherit; + text-decoration: underline; + + &:hover { text-decoration: none; } + } + + .glimmer-icon { + svg { + @include square-px-rem(10px); + @include rem-fallback(margin-left, -10px); + @include rem-fallback(margin-top, -18px); + } + } + @include themify($themes) { .pcs-renderer-icon { margin-right: 10px; @@ -56,7 +72,6 @@ $iconSize: 14px; .pcs-renderer-text { color: themed('colorCellRendererText'); } &.highlight { - .pcs-renderer-icon svg { fill: themed('colorCellRendererSvgFillHighlight'); @@ -64,17 +79,10 @@ $iconSize: 14px; } .pcs-renderer-text { color: themed('colorCellRendererTextHighlight'); } - } - } -} - -.soft-select-link-cell { - display: inline; - color: inherit; - text-decoration: underline; - &:hover { text-decoration: none; } + .glimmer-icon svg { fill: themed('colorGlimmerSvgFill'); } + } } .ag-row { diff --git a/src/components/shared/cellRenderers/glimmerRenderer/glimmerRenderer.js b/src/components/shared/cellRenderers/glimmerRenderer/glimmerRenderer.js new file mode 100644 index 000000000..1ef14e7ef --- /dev/null +++ b/src/components/shared/cellRenderers/glimmerRenderer/glimmerRenderer.js @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from "react"; + +import { svgs } from 'utilities'; +import { Svg } from 'components/shared/svg/svg'; + +import '../cellRenderer.css'; + +export const GlimmerRenderer = (props) => ( + props.value + ? + : null +); diff --git a/src/components/shared/cellRenderers/index.js b/src/components/shared/cellRenderers/index.js index 88a3a913a..c24bc9bdc 100644 --- a/src/components/shared/cellRenderers/index.js +++ b/src/components/shared/cellRenderers/index.js @@ -3,6 +3,7 @@ export * from './connectionStatusRenderer/connectionStatusRenderer'; export * from './countRenderer/countRenderer'; export * from './isSimulatedRenderer/isSimulatedRenderer'; +export * from './glimmerRenderer/glimmerRenderer'; export * from './lastTriggerRenderer/lastTriggerRenderer'; export * from './linkRenderer/linkRenderer'; export * from './ruleStatusRenderer/ruleStatusRenderer'; diff --git a/src/components/shared/cellRenderers/softSelectLinkRenderer/softSelectLinkRenderer.js b/src/components/shared/cellRenderers/softSelectLinkRenderer/softSelectLinkRenderer.js index 794e57ff1..70bf2f8c7 100644 --- a/src/components/shared/cellRenderers/softSelectLinkRenderer/softSelectLinkRenderer.js +++ b/src/components/shared/cellRenderers/softSelectLinkRenderer/softSelectLinkRenderer.js @@ -3,6 +3,7 @@ import React, { Component } from "react"; import { isFunc } from 'utilities'; +import { GlimmerRenderer } from 'components/shared/cellRenderers'; import '../cellRenderer.css'; @@ -16,11 +17,16 @@ export class SoftSelectLinkRenderer extends Component { }; render() { - const { value, context } = this.props; + const { value, context, data } = this.props; return ( - isFunc(context.onSoftSelectChange) - ? { value } - : value +
+ + { + isFunc(context.onSoftSelectChange) + ? {value} + : value + } +
); } } diff --git a/src/store/reducers/devicesReducer.js b/src/store/reducers/devicesReducer.js index e8ce1b8f3..e024d96c2 100644 --- a/src/store/reducers/devicesReducer.js +++ b/src/store/reducers/devicesReducer.js @@ -81,7 +81,8 @@ const deleteDevicesReducer = (state, { payload }) => { }; const insertDevicesReducer = (state, { payload }) => { - const { entities: { devices }, result } = normalize(payload, deviceListSchema); + const inserted = payload.map(device => ({ ...device, isNew: true })); + const { entities: { devices }, result } = normalize(inserted, deviceListSchema); if (state.entities) { return update(state, { entities: { $merge: devices }, diff --git a/src/store/reducers/packagesReducer.js b/src/store/reducers/packagesReducer.js index 50249d803..2c2ed9253 100644 --- a/src/store/reducers/packagesReducer.js +++ b/src/store/reducers/packagesReducer.js @@ -60,7 +60,8 @@ const packageListSchema = new schema.Array(packageSchema); const initialState = { ...errorPendingInitialState, entities: {} }; const insertPackageReducer = (state, { payload, fromAction }) => { - const { entities: { packages }, result } = normalize(payload, packageSchema); + const { entities: { packages }, result } = normalize({...payload, isNew: true}, packageSchema); + if (state.entities) { return update(state, { entities: { $merge: packages }, diff --git a/src/store/reducers/rulesReducer.js b/src/store/reducers/rulesReducer.js index 198963572..ed6ca08e3 100644 --- a/src/store/reducers/rulesReducer.js +++ b/src/store/reducers/rulesReducer.js @@ -34,7 +34,7 @@ export const epics = createEpicScenario({ fetchRules: { type: 'RULES_FETCH', epic: fromAction => - TelemetryService.getRules({includeDeleted: true}) + TelemetryService.getRules({ includeDeleted: true }) .flatMap(rules => Observable.from(rules) .flatMap(({ id, groupId }) => [ @@ -93,7 +93,8 @@ const ruleListSchema = new schema.Array(ruleSchema); const initialState = { ...errorPendingInitialState, entities: {}, items: [] }; const insertRulesReducer = (state, { payload }) => { - const { entities: { rules }, result } = normalize(payload, ruleListSchema); + const inserted = payload.map(rule => ({ ...rule, isNew: true })); + const { entities: { rules }, result } = normalize(inserted, ruleListSchema); if (state.entities) { return update(state, { entities: { $merge: rules }, diff --git a/src/styles/_themes.scss b/src/styles/_themes.scss index 4ff8f2963..61013663f 100644 --- a/src/styles/_themes.scss +++ b/src/styles/_themes.scss @@ -32,6 +32,8 @@ $themes: ( colorCellRendererText: $colorSmoke, colorCellRendererTextHighlight: $colorWhite, + colorGlimmerSvgFill: #f4f4f4, + gridSortIcon: '~assets/icons/sort_dark.svg', gridAscIcon: '~assets/icons/sort_a2z_dark.svg', gridDescIcon: '~assets/icons/sort_z2a_dark.svg', @@ -191,6 +193,8 @@ $themes: ( colorCellRendererText: #333, colorCellRendererTextHighlight: #333, + colorGlimmerSvgFill: #212121, + gridSortIcon: '~assets/icons/sort_light.svg', gridAscIcon: '~assets/icons/sort_a2z_light.svg', gridDescIcon: '~assets/icons/sort_z2a_light.svg', diff --git a/src/utilities/svgs.js b/src/utilities/svgs.js index 7b4ed52f7..57cce81e7 100644 --- a/src/utilities/svgs.js +++ b/src/utilities/svgs.js @@ -25,6 +25,7 @@ import EditIconPath from 'assets/icons/edit.svg'; import EllipsisIconPath from 'assets/icons/ellipsis.svg'; import EnableToggleIconPath from 'assets/icons/enableToggle.svg'; import ErrorIconPath from 'assets/icons/errorAsterisk.svg'; +import GlimmerIconPath from 'assets/icons/glimmer.svg'; import HamburgerIconPath from 'assets/icons/hamburger.svg'; import InfoBubbleIconPath from 'assets/icons/infoBubble.svg'; import InfoIconPath from 'assets/icons/info.svg'; @@ -93,6 +94,7 @@ export const svgs = { ellipsis: EllipsisIconPath, enableToggle: EnableToggleIconPath, error: ErrorIconPath, + glimmer: GlimmerIconPath, hamburger: HamburgerIconPath, info: InfoIconPath, infoBubble: InfoBubbleIconPath, From 7d3eb23440b08b6b67aa63d17b72c44c31d092a8 Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 14 Sep 2018 11:14:17 -0700 Subject: [PATCH 11/25] Add new Deployment (#1096) * Add new Deployment * Fix for ME's review coments * DevicesByCondition * undefined * Fix for ME's 2nd comments * alphabetical order --- public/locales/en/translations.json | 47 ++- .../deployments/deployments.container.js | 24 +- .../pages/deployments/deployments.js | 82 ++++- .../pages/deployments/deployments.scss | 4 +- .../deploymentsGrid/deploymentsGrid.js | 57 ++++ .../deploymentsGrid/deploymentsGridConfig.js | 62 ++++ .../deployments/deploymentsGrid/index.js | 4 + .../deploymentDelete.container.js | 23 ++ .../deploymentDelete/deploymentDelete.js | 41 +++ .../deploymentDelete/deploymentDelete.scss | 9 + .../flyouts/deploymentDelete/index.js | 4 + .../deploymentNew/deploymentNew.container.js | 51 +++ .../flyouts/deploymentNew/deploymentNew.js | 317 ++++++++++++++++++ .../flyouts/deploymentNew/deploymentNew.scss | 31 ++ .../flyouts/deploymentNew/index.js | 4 + .../pages/deployments/flyouts/index.js | 4 + src/components/pages/devices/devices.js | 10 +- .../packageNew/packageNew.container.js | 6 +- .../packages/flyouts/packageNew/packageNew.js | 4 + src/components/pages/packages/packages.scss | 4 - src/components/pages/rules/rules.js | 5 - .../shared/pageTitle/pageTitle.scss | 1 + src/services/iotHubManagerService.js | 29 +- src/services/models/iotHubManagerModels.js | 25 ++ src/store/reducers/deploymentsReducer.js | 146 ++++++++ src/store/reducers/devicesReducer.js | 19 +- src/store/reducers/packagesReducer.js | 2 + src/store/rootEpic.js | 2 + src/store/rootReducer.js | 6 +- src/store/utilities.js | 5 + 30 files changed, 994 insertions(+), 34 deletions(-) create mode 100644 src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js create mode 100644 src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js create mode 100644 src/components/pages/deployments/deploymentsGrid/index.js create mode 100644 src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.container.js create mode 100644 src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.js create mode 100644 src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.scss create mode 100644 src/components/pages/deployments/flyouts/deploymentDelete/index.js create mode 100644 src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.container.js create mode 100644 src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js create mode 100644 src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.scss create mode 100644 src/components/pages/deployments/flyouts/deploymentNew/index.js create mode 100644 src/components/pages/deployments/flyouts/index.js create mode 100644 src/store/reducers/deploymentsReducer.js diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 7157b1773..5e35b59f5 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -540,6 +540,50 @@ "P7D": "Last week", "P1M": "Last month" }, + "deployments": { + "title": "Deployments", + "flyouts": { + "new": { + "contextMenuName": "New deployment", + "title": "New deployment", + "apply": "Apply", + "cancel": "Cancel", + "close": "Close", + "type": "Package type", + "deviceGroup": "Device Group", + "name": "Name", + "priority": "Priority", + "package": "Package", + "typePlaceHolder": "Select package type", + "packagePlaceHolder": "Select package", + "deviceGroupPlaceHolder": "Select device group", + "priorityPlaceHolder": "Enter priority", + "namePlaceHolder": "Enter name", + "targetText": "targeted devices", + "infoText": "* Edge Manifest packages that you deploy will be applied to every edge device group in the device group and to any edge group that you add to the device group in the future. The deployment will run continously, verifying that current and future devices are configured correctly (and automatically updating that aren't). Package wont be applied to non-edge devices.", + "successText": "View your deployment status detail for {{deplymentName}}", + "creating": "Creating deployment", + "validation": { + "required": "Is required", + "nan": "Must be a number" + } + } + }, + "typeOptions": { + "edgemanifest": "Edge Manifest" + }, + "grid": { + "name": "Name", + "package": "Package", + "deviceGroup": "Device group", + "priority": "Priority", + "type": "Package Type", + "applied": "Applied", + "failed": "Failed", + "succeeded": "Succeeded", + "dateCreated": "Created On" + } + }, "packages": { "searchPlaceholder": "Search packages...", "noneFound": "No packages found.", @@ -581,9 +625,6 @@ "edgemanifest": "Edge Manifest" } }, - "deployments": { - "header": "Deployments" - }, "walkthrough": { "tabs": { "dashboard": "Dashboard", diff --git a/src/components/pages/deployments/deployments.container.js b/src/components/pages/deployments/deployments.container.js index a75ff5db2..f8f878974 100644 --- a/src/components/pages/deployments/deployments.container.js +++ b/src/components/pages/deployments/deployments.container.js @@ -1,9 +1,27 @@ // Copyright (c) Microsoft. All rights reserved. import { translate } from 'react-i18next'; - +import { connect } from 'react-redux'; import { Deployments } from './deployments'; +import { + getDeploymentsError, + getDeploymentsPendingStatus, + getDeployments, + getDeploymentsLastUpdated, + epics as deploymentsEpics +} from 'store/reducers/deploymentsReducer'; + +// Pass the global info needed +const mapStateToProps = state => ({ + isPending: getDeploymentsPendingStatus(state), + error: getDeploymentsError(state), + deployments: getDeployments(state), + lastUpdated: getDeploymentsLastUpdated(state) +}); -// TODO +// Wrap the dispatch methods +const mapDispatchToProps = dispatch => ({ + fetchDeployments: () => dispatch(deploymentsEpics.actions.fetchDeployments()) +}); -export const DeploymentsContainer = translate()(Deployments); +export const DeploymentsContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Deployments)); diff --git a/src/components/pages/deployments/deployments.js b/src/components/pages/deployments/deployments.js index 0b605cb0c..05005a434 100644 --- a/src/components/pages/deployments/deployments.js +++ b/src/components/pages/deployments/deployments.js @@ -2,19 +2,87 @@ import React, { Component } from 'react'; -import { PageContent } from 'components/shared'; +import { permissions } from 'services/models'; +import { + AjaxError, + Btn, + ContextMenu, + PageContent, + Protected, + RefreshBar, + PageTitle +} from 'components/shared'; +import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/shell/deviceGroupDropdown'; +import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/shell/manageDeviceGroupsBtn'; +import { DeploymentsGrid } from './deploymentsGrid'; +import { DeploymentNewContainer } from './flyouts'; +import { svgs } from 'utilities'; import './deployments.css'; +const closedFlyoutState = { openFlyoutName: undefined }; + export class Deployments extends Component { - // TODO + constructor(props) { + super(props); + this.state = { + ...closedFlyoutState, + contextBtns: null + }; + + if (!this.props.lastUpdated && !this.props.error) { + this.props.fetchDeployments(); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.isPending && nextProps.isPending !== this.props.isPending) { + // If the grid data refreshes, hide the flyout + this.setState(closedFlyoutState); + } + } + + closeFlyout = () => this.setState(closedFlyoutState); + + onContextMenuChange = contextBtns => this.setState({ + contextBtns, + openFlyoutName: undefined + }); + + openNewDeploymentFlyout = () => this.setState({ + openFlyoutName: 'newDeployment' + }); + + onGridReady = gridReadyEvent => this.deploymentGridApi = gridReadyEvent.api; render() { - const { t } = this.props; - return ( - - {t('deployments.header')} + const { t, deployments, error, isPending, fetchDeployments, lastUpdated } = this.props; + const gridProps = { + onGridReady: this.onGridReady, + rowData: isPending ? undefined : deployments || [], + refresh: fetchDeployments, + onContextMenuChange: this.onContextMenuChange, + t: t + }; + + return [ + + + + {this.state.contextBtns} + + {t('deployments.flyouts.new.contextMenuName')} + + + + + , + + + {!!error && } + {!error && } + {this.state.openFlyoutName === 'newDeployment' && } - ); + ]; } } diff --git a/src/components/pages/deployments/deployments.scss b/src/components/pages/deployments/deployments.scss index a5debe8d8..7f5d37d16 100644 --- a/src/components/pages/deployments/deployments.scss +++ b/src/components/pages/deployments/deployments.scss @@ -5,5 +5,7 @@ @import 'src/styles/themes'; .deployments-page-container { - // TODO + display: flex; + flex-flow: column nowrap; + padding: $baseContentPadding; } diff --git a/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js b/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js new file mode 100644 index 000000000..317a65703 --- /dev/null +++ b/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +import React, { Component } from 'react'; +import { deploymentsColumnDefs, defaultDeploymentsGridProps } from './deploymentsGridConfig'; +import { PcsGrid } from 'components/shared'; +import { isFunc, translateColumnDefs } from 'utilities'; + +export class DeploymentsGrid extends Component { + constructor(props) { + super(props); + + this.columnDefs = [ + deploymentsColumnDefs.name, + deploymentsColumnDefs.package, + deploymentsColumnDefs.deviceGroup, + deploymentsColumnDefs.priority, + deploymentsColumnDefs.type, + deploymentsColumnDefs.applied, + deploymentsColumnDefs.succeeded, + deploymentsColumnDefs.failed, + deploymentsColumnDefs.dateCreated, + ]; + } + /** + * Get the grid api options + * + * @param {Object} gridReadyEvent An object containing access to the grid APIs + */ + onGridReady = gridReadyEvent => { + this.deploymentsGridApi = gridReadyEvent.api; + // Call the onReady props if it exists + if (isFunc(this.props.onGridReady)) { + this.props.onGridReady(gridReadyEvent); + } + }; + + render() { + const gridProps = { + /* Grid Properties */ + ...defaultDeploymentsGridProps, + columnDefs: translateColumnDefs(this.props.t, this.columnDefs), + sizeColumnsToFit: true, + deltaRowDataMode: true, + ...this.props, // Allow default property overrides + onGridReady: event => this.onGridReady(event), // Wrap in a function to avoid closure issues + getRowNodeId: ({ id }) => id, + enableSorting: true, + unSortIcon: true, + context: { + t: this.props.t + } + }; + + return ([ + + ]); + } +} diff --git a/src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js b/src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js new file mode 100644 index 000000000..20b9c7f8c --- /dev/null +++ b/src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +import Config from 'app.config'; +import { TimeRenderer } from 'components/shared/cellRenderers'; +import { gridValueFormatters } from 'components/shared/pcsGrid/pcsGridConfig'; + +const { checkForEmpty } = gridValueFormatters; + +export const deploymentsColumnDefs = { + name: { + headerName: 'deployments.grid.name', + field: 'name', + sort: 'asc', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + package: { + headerName: 'deployments.grid.package', + field: 'packageId', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + deviceGroup: { + headerName: 'deployments.grid.deviceGroup', + field: 'deviceGroupId', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + priority: { + headerName: 'deployments.grid.priority', + field: 'priority', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + type: { + headerName: 'deployments.grid.type', + field: 'type', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + applied: { + headerName: 'deployments.grid.applied', + field: 'appliedCount', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + failed: { + headerName: 'deployments.grid.failed', + field: 'failedCount', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + succeeded: { + headerName: 'deployments.grid.succeeded', + field: 'succeededCount', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + dateCreated: { + headerName: 'deployments.grid.dateCreated', + field: 'createdDateTimeUtc', + cellRendererFramework: TimeRenderer + } +}; + +export const defaultDeploymentsGridProps = { + enableColResize: true, + pagination: true, + paginationPageSize: Config.paginationPageSize +}; diff --git a/src/components/pages/deployments/deploymentsGrid/index.js b/src/components/pages/deployments/deploymentsGrid/index.js new file mode 100644 index 000000000..2dd53da31 --- /dev/null +++ b/src/components/pages/deployments/deploymentsGrid/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './deploymentsGridConfig'; +export * from './deploymentsGrid'; diff --git a/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.container.js b/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.container.js new file mode 100644 index 000000000..f4e8f719e --- /dev/null +++ b/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.container.js @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { DeploymentDelete } from './deploymentDelete'; +import { + getDeleteDeploymentError, + getDeleteDeploymentPendingStatus, + epics as deploymentsEpics +} from 'store/reducers/deploymentsReducer'; + +// Pass the global info needed +const mapStateToProps = state => ({ + isPending: getDeleteDeploymentPendingStatus(state), + error: getDeleteDeploymentError(state) +}); + +// Wrap the dispatch methods +const mapDispatchToProps = dispatch => ({ + deleteDeployment: deploymentId => dispatch(deploymentsEpics.actions.deleteDeployment(deploymentId)) +}); + +export const DeploymentDeleteContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeploymentDelete)); diff --git a/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.js b/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.js new file mode 100644 index 000000000..4e823f9e3 --- /dev/null +++ b/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.js @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; + +import { LinkedComponent } from 'utilities'; +import { Modal } from 'components/shared'; + +import './deploymentDelete.css'; + +export class DeploymentDelete extends LinkedComponent { + + constructor(props) { + super(props); + + this.state = { + changesApplied: false + }; + } + + componentWillReceiveProps({ error, isPending, onClose }) { + if (this.state.changesApplied && !error && !isPending) { + onClose(); + } + } + + apply = () => { + const { deletePackage, package: { id } } = this.props; + deletePackage(id); + this.setState({ changesApplied: true }); + } + + render() { + const { onClose } = this.props; + + return ( + + TODO + + ); + } +} diff --git a/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.scss b/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.scss new file mode 100644 index 000000000..887bad0f9 --- /dev/null +++ b/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.scss @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/variables'; +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.delete-deployment-container { + // TODO - add this in modal -> @include rem-fallback(font-size, 14px); +} diff --git a/src/components/pages/deployments/flyouts/deploymentDelete/index.js b/src/components/pages/deployments/flyouts/deploymentDelete/index.js new file mode 100644 index 000000000..5940b42f3 --- /dev/null +++ b/src/components/pages/deployments/flyouts/deploymentDelete/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './deploymentDelete'; +export * from './deploymentDelete.container'; diff --git a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.container.js b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.container.js new file mode 100644 index 000000000..01e0838ee --- /dev/null +++ b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.container.js @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { DeploymentNew } from './deploymentNew'; +import { + getCreateDeploymentError, + getCreateDeploymentPendingStatus, + epics as deploymentsEpics, + redux as deploymentsRedux +} from 'store/reducers/deploymentsReducer'; +import { + getPackages, + getPackagesPendingStatus, + getPackagesError, + epics as packagesEpics, + redux as packagesRedux +} from 'store/reducers/packagesReducer'; +import { getDeviceGroups } from 'store/reducers/appReducer'; +import { + getDevices, + getDevicesByConditionError, + getDevicesByConditionPendingStatus, + epics as devicesEpics, + redux as devicesRedux +} from 'store/reducers/devicesReducer'; + +// Pass the global info needed +const mapStateToProps = state => ({ + packages: getPackages(state), + packagesPending: getPackagesPendingStatus(state), + packagesError: getPackagesError(state), + deviceGroups: getDeviceGroups(state), + devices: getDevices(state), + devicesPending: getDevicesByConditionPendingStatus(state), + devicesError: getDevicesByConditionError(state), + createIsPending: getCreateDeploymentPendingStatus(state), + createError: getCreateDeploymentError(state) +}); + +// Wrap the dispatch methods +const mapDispatchToProps = dispatch => ({ + createDeployment: deploymentModel => dispatch(deploymentsEpics.actions.createDeployment(deploymentModel)), + resetCreatePendingError: () => dispatch(deploymentsRedux.actions.resetPendingAndError(deploymentsEpics.actions.createDeployment)), + fetchPackages: () => dispatch(packagesEpics.actions.fetchPackages()), + resetPackagesPendingError: () => dispatch(packagesRedux.actions.resetPendingAndError(packagesEpics.actions.fetchPackages)), + fetchDevices: condition => dispatch(devicesEpics.actions.fetchDevicesByCondition(condition)), + resetDevicesPendingError: () => dispatch(devicesRedux.actions.resetPendingAndError(devicesEpics.actions.fetchDevicesByCondition)) +}); + +export const DeploymentNewContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeploymentNew)); diff --git a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js new file mode 100644 index 000000000..43d516dd3 --- /dev/null +++ b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; + +import { packageTypeOptions } from 'services/models'; +import { svgs, LinkedComponent, Validator } from 'utilities'; +import { + AjaxError, + Btn, + BtnToolbar, + Flyout, + FlyoutHeader, + FlyoutTitle, + FlyoutCloseBtn, + FlyoutContent, + Indicator, + FormControl, + FormGroup, + FormLabel, + SummaryBody, + SectionDesc, + SummaryCount, + SummarySection, + Svg +} from 'components/shared'; + +import './deploymentNew.css'; + +export class DeploymentNew extends LinkedComponent { + constructor(props) { + super(props); + + this.state = { + packageType: undefined, + deviceGroupId: undefined, + deviceGroupName: '', + name: '', + priority: '', + packageId: undefined, + packageName: '', + targetedDeviceCount: '', + edgePackageSelected: false, + changesApplied: false + }; + } + + componentWillReceiveProps(nextProps) { + const { devices, packages, deviceGroups } = nextProps; + const { deviceGroupId, packageId } = this.state; + if (devices && devices.length > 0) { + this.setState({ targetedDeviceCount: devices.length }); + } + if (deviceGroupId !== undefined) { + const deviceGroupName = deviceGroups.find(deviceGroup => deviceGroup.id === deviceGroupId).displayName; + this.setState({ deviceGroupName }); + } + if (packageId !== undefined) { + const packageName = packages.find(packageItem => packageItem.id === packageId).name; + this.setState({ packageName }); + } + } + + componentWillUnmount() { + const { resetCreatePendingError, resetPackagesPendingError, resetDevicesPendingError } = this.props; + resetCreatePendingError(); + resetPackagesPendingError(); + resetDevicesPendingError(); + } + + apply = (event) => { + event.preventDefault(); + const { createDeployment } = this.props; + const { packageType, deviceGroupId, name, priority, packageId } = this.state; + if (this.formIsValid()) { + createDeployment({ 'type': packageType, deviceGroupId, name, priority, packageId }); + this.setState({ changesApplied: true }); + } + } + + formIsValid = () => { + return [ + this.packageTypeLink, + this.nameLink, + this.deviceGroupIdLink, + this.priorityLink, + this.packageIdLink + ].every(link => !link.error); + } + + onPackageSelected = (e) => { + switch (e.target.value.value) { + // case Edge manifest + case 'EdgeManifest': + const { fetchPackages } = this.props; + fetchPackages(); + this.setState({ edgePackageSelected: true }); + break; + // other cases to be impletmented in Edge walk iteration + default: + break; + } + this.formControlChange(); + } + + onDeviceGroupSelected = (e) => { + const { fetchDevices, deviceGroups } = this.props; + const selectedDeviceGroupId = e.target.value.value; + const selectedDeviceGroup = deviceGroups.find(deviceGroup => deviceGroup.id === selectedDeviceGroupId) + fetchDevices(selectedDeviceGroup.conditions); + } + + formControlChange = () => { + if (this.state.changesApplied) { + this.setState({ changesApplied: false }); + } + } + + toPackageSelectOption = ({ id, name }) => ({ label: name, value: id }); + + toDeviceGroupSelectOption = ({ id, displayName }) => ({ label: displayName, value: id }); + + render() { + const { + t, + onClose, + createIsPending, + createError, + packagesPending, + packagesError, + packages, + deviceGroups, + devicesPending, + devicesError + } = this.props; + const { + name, + packageType, + deviceGroupId, + deviceGroupName, + packageName, + priority, + targetedDeviceCount, + changesApplied, + edgePackageSelected, + } = this.state; + + // Validators + const requiredValidator = (new Validator()).check(Validator.notEmpty, t('deployments.flyouts.new.validation.required')); + + // Links + this.packageTypeLink = this.linkTo('packageType').map(({ value }) => value).withValidator(requiredValidator); + this.nameLink = this.linkTo('name').withValidator(requiredValidator); + this.deviceGroupIdLink = this.linkTo('deviceGroupId').map(({ value }) => value).withValidator(requiredValidator); + this.priorityLink = this.linkTo('priority') + .check(Validator.notEmpty, () => this.props.t('deployments.flyouts.new.validation.required')) + .check(val => !isNaN(val), t('deployments.flyouts.new.validation.nan')); + this.packageIdLink = this.linkTo('packageId').map(({ value }) => value).withValidator(requiredValidator); + + const isPackageTypeSelected = packageType !== undefined; + const isDeviceGroupSelected = deviceGroupId !== undefined; + const packageOptions = packages.map(this.toPackageSelectOption); + const deviceGroupOptions = deviceGroups.map(this.toDeviceGroupSelectOption); + const typeOptions = packageTypeOptions.map(value => ({ + label: t(`deployments.typeOptions.${value.toLowerCase()}`), + value + })); + const completedSuccessfully = changesApplied && !createError && !createIsPending; + const deviceFetchSuccessful = isDeviceGroupSelected && !devicesError && !devicesPending; + + return ( + + + {t('deployments.flyouts.new.title')} + + + +
+ + {t('deployments.flyouts.new.name')} + { + !completedSuccessfully && + + } + { + completedSuccessfully && {name} + } + + + {t('deployments.flyouts.new.priority')} + { + !completedSuccessfully && + + } + { + completedSuccessfully && {priority} + } + + + {t('deployments.flyouts.new.type')} + { + !completedSuccessfully && + + } + { + completedSuccessfully && {packageType} + } + + + {t('deployments.flyouts.new.package')} + {!packagesPending && !completedSuccessfully && + + } + { + packagesPending && + } + {/** Displays an error message if one occurs while fetching packages. */ + packagesError && + } + { + completedSuccessfully && {packageName} + } + + + {t('deployments.flyouts.new.deviceGroup')} + { + !completedSuccessfully && + + } + { + completedSuccessfully && {deviceGroupName} + } + + + + {/** Displays targeted devices count once device goup is selected. */ + deviceFetchSuccessful && + [ + {targetedDeviceCount}, + {t('deployments.flyouts.new.targetText')}, + completedSuccessfully && , + ] + } + {createIsPending && [, t('deployments.flyouts.new.creating')]} + + + {/** Displays a info message if package type selected is edge Manifest */ + !changesApplied && edgePackageSelected && +
+ {t('deployments.flyouts.new.infoText')} +
+ } + {/** Displays a success message if deployment is created successfully */ + completedSuccessfully && +
+ {t('deployments.flyouts.new.successText', { deplymentName: name })} +
+ } + {/** Displays an error message if one occurs while creating deployment. */ + changesApplied && createError && + } + { + /** If form is complete, show the buttons for creating a deployment and closing the flyout. */ + (!completedSuccessfully) && + + {t('deployments.flyouts.new.apply')} + {t('deployments.flyouts.new.cancel')} + + } + { + /** After successful deployment creation, show only close button. */ + (completedSuccessfully) && + + {t('deployments.flyouts.new.close')} + + } +
+
+
+
+ ); + } +} diff --git a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.scss b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.scss new file mode 100644 index 000000000..e167259ec --- /dev/null +++ b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.scss @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/variables'; +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.new-deployment-content { + @include rem-fallback(padding-top, 24px); + + .new-deployment-info-text, + .new-deployment-flyout-error { @include rem-fallback(padding-top, 24px); } + + .new-deployment-formGroup, + { + @include rem-fallback(padding-top, 12px); + @include rem-fallback(padding-bottom, 12px); + } + + .summary-icon svg { + @include square-px-rem(16px); + @include rem-fallback(margin-left, 8px); + } + + @include themify($themes) { + .summary-icon svg { fill: themed('colorContentText'); } + + .new-deployment-flyout-error { border-color: themed('colorAlert'); } + + .new-deployment-success-labels { color: themed('colorFlyoutText'); } + } +} diff --git a/src/components/pages/deployments/flyouts/deploymentNew/index.js b/src/components/pages/deployments/flyouts/deploymentNew/index.js new file mode 100644 index 000000000..fd6d1aa3b --- /dev/null +++ b/src/components/pages/deployments/flyouts/deploymentNew/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './deploymentNew'; +export * from './deploymentNew.container'; diff --git a/src/components/pages/deployments/flyouts/index.js b/src/components/pages/deployments/flyouts/index.js new file mode 100644 index 000000000..dfd6d6f60 --- /dev/null +++ b/src/components/pages/deployments/flyouts/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './deploymentNew'; +export * from './deploymentDelete'; diff --git a/src/components/pages/devices/devices.js b/src/components/pages/devices/devices.js index f5e779aad..0095c4024 100644 --- a/src/components/pages/devices/devices.js +++ b/src/components/pages/devices/devices.js @@ -78,7 +78,7 @@ export class Devices extends Component { - { this.state.contextBtns } + {this.state.contextBtns} {t('devices.flyouts.SIMManagement.title')} @@ -91,10 +91,10 @@ export class Devices extends Component { , - { !!error && } - { !error && } -{ newDeviceFlyoutOpen && } - { simManagementFlyoutOpen && } + {!!error && } + {!error && } + {newDeviceFlyoutOpen && } + {simManagementFlyoutOpen && } ]; } diff --git a/src/components/pages/packages/flyouts/packageNew/packageNew.container.js b/src/components/pages/packages/flyouts/packageNew/packageNew.container.js index f39194171..ef493cf44 100644 --- a/src/components/pages/packages/flyouts/packageNew/packageNew.container.js +++ b/src/components/pages/packages/flyouts/packageNew/packageNew.container.js @@ -6,7 +6,8 @@ import { PackageNew } from './packageNew'; import { getCreatePackageError, getCreatePackagePendingStatus, - epics as packagesEpics + epics as packagesEpics, + redux as packagesRedux } from 'store/reducers/packagesReducer'; // Pass the global info needed @@ -17,7 +18,8 @@ const mapStateToProps = state => ({ // Wrap the dispatch methods const mapDispatchToProps = dispatch => ({ - createPackage: packageModel => dispatch(packagesEpics.actions.createPackage(packageModel)) + createPackage: packageModel => dispatch(packagesEpics.actions.createPackage(packageModel)), + resetPackagesPendingError: () => dispatch(packagesRedux.actions.resetPendingAndError(packagesEpics.actions.createPackage)), }); export const PackageNewContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(PackageNew)); diff --git a/src/components/pages/packages/flyouts/packageNew/packageNew.js b/src/components/pages/packages/flyouts/packageNew/packageNew.js index 227da6166..a7eb5a271 100644 --- a/src/components/pages/packages/flyouts/packageNew/packageNew.js +++ b/src/components/pages/packages/flyouts/packageNew/packageNew.js @@ -39,6 +39,10 @@ export class PackageNew extends LinkedComponent { }; } + componentWillUnmount() { + this.props.resetPackagesPendingError(); + } + apply = (event) => { event.preventDefault(); const { createPackage } = this.props; diff --git a/src/components/pages/packages/packages.scss b/src/components/pages/packages/packages.scss index d01067d73..da5440ae5 100644 --- a/src/components/pages/packages/packages.scss +++ b/src/components/pages/packages/packages.scss @@ -8,8 +8,4 @@ display: flex; flex-flow: column nowrap; padding: $baseContentPadding; - - .package-title { - @include rem-fallback(padding-bottom, 30px); - } } diff --git a/src/components/pages/rules/rules.js b/src/components/pages/rules/rules.js index 31deedf75..b97f64967 100644 --- a/src/components/pages/rules/rules.js +++ b/src/components/pages/rules/rules.js @@ -47,11 +47,6 @@ export class Rules extends Component { } } - changeDeviceGroup = () => { - const { changeDeviceGroup, deviceGroups } = this.props; - changeDeviceGroup(deviceGroups[1].id); - } - closeFlyout = () => this.setState(closedFlyoutState); openNewRuleFlyout = () => { diff --git a/src/components/shared/pageTitle/pageTitle.scss b/src/components/shared/pageTitle/pageTitle.scss index 5e7d30632..96f2393fa 100644 --- a/src/components/shared/pageTitle/pageTitle.scss +++ b/src/components/shared/pageTitle/pageTitle.scss @@ -9,6 +9,7 @@ margin: 0; @include rem-fallback(margin-right, 10px); @include rem-font-size(48px); + @include rem-fallback(padding-bottom, 30px); @include themify($themes) { color: themed('colorHeaderText'); diff --git a/src/services/iotHubManagerService.js b/src/services/iotHubManagerService.js index c9998dff0..2bee177a4 100644 --- a/src/services/iotHubManagerService.js +++ b/src/services/iotHubManagerService.js @@ -5,7 +5,16 @@ import { Observable } from 'rxjs'; import Config from 'app.config'; import { stringify } from 'query-string'; import { HttpClient } from 'utilities/httpClient'; -import { toDevicesModel, toDeviceModel, toJobsModel, toJobStatusModel, toDevicePropertiesModel } from './models'; +import { + toDevicesModel, + toDeviceModel, + toJobsModel, + toJobStatusModel, + toDevicePropertiesModel, + toDeploymentModel, + toDeploymentsModel, + toDeploymentRequestModel +} from './models'; const ENDPOINT = Config.serviceUrls.iotHubManager; @@ -58,4 +67,22 @@ export class IoTHubManagerService { ) .map(([iotResponse, dsResponse]) => toDevicePropertiesModel(iotResponse, dsResponse)); } + + /** Returns deployments */ + static getDeployments() { + return HttpClient.get(`${ENDPOINT}deployments`) + .map(toDeploymentsModel); + } + + /** Create a deployment */ + static createDeployment(deploymentModel) { + return HttpClient.post(`${ENDPOINT}deployments`, toDeploymentRequestModel(deploymentModel)) + .map(toDeploymentModel); + } + + /** Delete a deployment */ + static deleteDeployment(id) { + return HttpClient.delete(`${ENDPOINT}deployments${id}`) + .map(() => id); + } } diff --git a/src/services/models/iotHubManagerModels.js b/src/services/models/iotHubManagerModels.js index b50e5259a..3e503bcc5 100644 --- a/src/services/models/iotHubManagerModels.js +++ b/src/services/models/iotHubManagerModels.js @@ -177,3 +177,28 @@ export const toDevicePropertiesModel = (iotResponse, dsResponse) => { const propertySet = new Set([...getItems(iotResponse), ...getItems(dsResponse)]); return [...propertySet]; }; + +export const toDeploymentModel = (deployment = {}) => camelCaseReshape(deployment, { + 'id': 'id', + 'name': 'name', + 'deviceGroupId': 'deviceGroupId', + 'packageId': 'packageId', + 'priority': 'priority', + 'type': 'type', + 'createdDateTimeUtc': 'createdDateTimeUtc', + 'metrics.appliedCount': 'appliedCount', + 'metrics.failedCount': 'failedCount', + 'metrics.succeededCount': 'succeededCount', + 'metrics.targetedCount': 'targetedCount' +}); + +export const toDeploymentsModel = (response = {}) => getItems(response) + .map(toDeploymentModel); + +export const toDeploymentRequestModel = (deploymentModel = {}) => ({ + DeviceGroupId: deploymentModel.deviceGroupId, + Name: deploymentModel.name, + PackageId: deploymentModel.packageId, + Priority: deploymentModel.priority, + Type: deploymentModel.type +}); diff --git a/src/store/reducers/deploymentsReducer.js b/src/store/reducers/deploymentsReducer.js new file mode 100644 index 000000000..36f439c86 --- /dev/null +++ b/src/store/reducers/deploymentsReducer.js @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft. All rights reserved. + +import 'rxjs'; +import { Observable } from 'rxjs'; +import moment from 'moment'; +import { schema, normalize } from 'normalizr'; +import update from 'immutability-helper'; +import { createSelector } from 'reselect'; +import { IoTHubManagerService } from 'services'; +import { getActiveDeviceGroupId, getActiveDeviceGroupConditions } from './appReducer'; +import { + createReducerScenario, + createEpicScenario, + errorPendingInitialState, + pendingReducer, + errorReducer, + setPending, + resetPendingAndErrorReducer, + getPending, + getError, + toActionCreator +} from 'store/utilities'; + +// ========================= Epics - START +const handleError = fromAction => error => + Observable.of(redux.actions.registerError(fromAction.type, { error, fromAction })); + +export const epics = createEpicScenario({ + /** Loads Deployments */ + fetchDeployments: { + type: 'DEPLOYMENTS_FETCH', + epic: fromAction => + IoTHubManagerService.getDeployments() + .map(toActionCreator(redux.actions.updateDeployments, fromAction)) + .catch(handleError(fromAction)) + }, + /** Create a new deployment */ + createDeployment: { + type: 'DEPLOYMENTS_CREATE', + epic: fromAction => + IoTHubManagerService.createDeployment(fromAction.payload) + .map(toActionCreator(redux.actions.insertDeployment, fromAction)) + .catch(handleError(fromAction)) + }, + /** Delete deployment */ + deleteDeployment: { + type: 'DEPLOYMENTS_DELETE', + epic: fromAction => + IoTHubManagerService.deleteDeployment(fromAction.payload) + .map(toActionCreator(redux.actions.deleteDeployment, fromAction)) + .catch(handleError(fromAction)) + } +}); +// ========================= Epics - END + +// ========================= Schemas - START +const deploymentSchema = new schema.Entity('deployments'); +const deploymentListSchema = new schema.Array(deploymentSchema); +// ========================= Schemas - END + +// ========================= Reducers - START +const initialState = { ...errorPendingInitialState, entities: {} }; + +const insertDeploymentReducer = (state, { payload, fromAction }) => { + const { entities: { deployments }, result } = normalize(payload, deploymentSchema); + if (state.entities) { + return update(state, { + entities: { $merge: deployments }, + items: { $splice: [[0, 0, result]] }, + ...setPending(fromAction.type, false) + }); + } + return update(state, { + entities: { $set: deployments }, + items: { $set: [result] }, + ...setPending(fromAction.type, false) + }); +}; + +const deleteDeploymentReducer = (state, { payload, fromAction }) => { + const idx = state.items.indexOf(payload); + return update(state, { + entities: { $unset: [payload] }, + items: { $splice: [[idx, 1]] }, + ...setPending(fromAction.type, false) + }); +}; + +const updateDeploymentsReducer = (state, { payload, fromAction }) => { + const { entities: { deployments }, result } = normalize(payload, deploymentListSchema); + return update(state, { + entities: { $set: deployments }, + items: { $set: result }, + lastUpdated: { $set: moment() }, + ...setPending(fromAction.type, false) + }); +}; + +/* Action types that cause a pending flag */ +const fetchableTypes = [ + epics.actionTypes.fetchDeployments, + epics.actionTypes.createDeployment, + epics.actionTypes.deleteDeployment +]; + +export const redux = createReducerScenario({ + insertDeployment: { type: 'DEPLOYMENT_INSERT', reducer: insertDeploymentReducer }, + deleteDeployment: { type: 'DEPLOYMENTS_DELETE', reducer: deleteDeploymentReducer }, + updateDeployments: { type: 'DEPLOYMENTS_UPDATE', reducer: updateDeploymentsReducer }, + registerError: { type: 'DEPLOYMENTS_REDUCER_ERROR', reducer: errorReducer }, + resetPendingAndError: { type: 'DEPLOYMENTS_REDUCER_RESET_ERROR_PENDING', reducer: resetPendingAndErrorReducer }, + isFetching: { multiType: fetchableTypes, reducer: pendingReducer } +}); + +export const reducer = { deployments: redux.getReducer(initialState) }; +// ========================= Reducers - END + +// ========================= Selectors - START +export const getDeploymentsReducer = state => state.deployments; +export const getEntities = state => getDeploymentsReducer(state).entities || {}; +export const getItems = state => getDeploymentsReducer(state).items || []; +export const getDeploymentsLastUpdated = state => getDeploymentsReducer(state).lastUpdated; +export const getDeploymentsError = state => + getError(getDeploymentsReducer(state), epics.actionTypes.fetchDeployments); +export const getDeploymentsPendingStatus = state => + getPending(getDeploymentsReducer(state), epics.actionTypes.fetchDeployments); +export const getCreateDeploymentError = state => + getError(getDeploymentsReducer(state), epics.actionTypes.createDeployment); +export const getCreateDeploymentPendingStatus = state => + getPending(getDeploymentsReducer(state), epics.actionTypes.createDeployment); +export const getDeleteDeploymentError = state => + getError(getDeploymentsReducer(state), epics.actionTypes.deleteDeployment); +export const getDeleteDeploymentPendingStatus = state => + getPending(getDeploymentsReducer(state), epics.actionTypes.deleteDeployment); +export const getDeployments = createSelector( + getEntities, getItems, getActiveDeviceGroupId, getActiveDeviceGroupConditions, + (entities, items, deviceGroupId, deviceGroupConditions = []) => + items.reduce((acc, id) => { + const deployment = entities[id]; + const activeDeviceGroup = deviceGroupConditions.length > 0 ? deviceGroupId : false; + return (deployment.deviceGroupId === activeDeviceGroup || !activeDeviceGroup) + ? [...acc, deployment] + : acc + }, []) +); +// ========================= Selectors - END diff --git a/src/store/reducers/devicesReducer.js b/src/store/reducers/devicesReducer.js index e024d96c2..6d0a3e4d8 100644 --- a/src/store/reducers/devicesReducer.js +++ b/src/store/reducers/devicesReducer.js @@ -11,6 +11,7 @@ import { IoTHubManagerService } from 'services'; import { createReducerScenario, createEpicScenario, + resetPendingAndErrorReducer, errorPendingInitialState, pendingReducer, errorReducer, @@ -36,6 +37,16 @@ export const epics = createEpicScenario({ } }, + /** Loads the devices by condition provided in payload*/ + fetchDevicesByCondition: { + type: 'DEVICES_FETCH_BY_CONDITION', + epic: fromAction => { + return IoTHubManagerService.getDevices(fromAction.payload) + .map(toActionCreator(redux.actions.updateDevices, fromAction)) + .catch(handleError(fromAction)) + } + }, + /* Update the devices if the selected device group changes */ refreshDevices: { type: 'DEVICES_REFRESH', @@ -133,7 +144,8 @@ const updatePropertiesReducer = (state, { payload }) => { /* Action types that cause a pending flag */ const fetchableTypes = [ - epics.actionTypes.fetchDevices + epics.actionTypes.fetchDevices, + epics.actionTypes.fetchDevicesByCondition ]; export const redux = createReducerScenario({ @@ -144,6 +156,7 @@ export const redux = createReducerScenario({ insertDevices: { type: 'DEVICE_INSERT', reducer: insertDevicesReducer }, updateTags: { type: 'DEVICE_UPDATE_TAGS', reducer: updateTagsReducer }, updateProperties: { type: 'DEVICE_UPDATE_PROPERTIES', reducer: updatePropertiesReducer }, + resetPendingAndError: { type: 'DEVICE_REDUCER_RESET_ERROR_PENDING', reducer: resetPendingAndErrorReducer } }); export const reducer = { devices: redux.getReducer(initialState) }; @@ -158,6 +171,10 @@ export const getDevicesError = state => getError(getDevicesReducer(state), epics.actionTypes.fetchDevices); export const getDevicesPendingStatus = state => getPending(getDevicesReducer(state), epics.actionTypes.fetchDevices); +export const getDevicesByConditionError = state => + getError(getDevicesReducer(state), epics.actionTypes.fetchDevicesByCondition); +export const getDevicesByConditionPendingStatus = state => + getPending(getDevicesReducer(state), epics.actionTypes.fetchDevicesByCondition); export const getDevices = createSelector( getEntities, getItems, (entities, items) => items.map(id => entities[id]) diff --git a/src/store/reducers/packagesReducer.js b/src/store/reducers/packagesReducer.js index 2c2ed9253..729834b73 100644 --- a/src/store/reducers/packagesReducer.js +++ b/src/store/reducers/packagesReducer.js @@ -11,6 +11,7 @@ import { createReducerScenario, createEpicScenario, errorPendingInitialState, + resetPendingAndErrorReducer, pendingReducer, errorReducer, setPending, @@ -107,6 +108,7 @@ export const redux = createReducerScenario({ deletePackage: { type: 'PACKAGES_DELETE', reducer: deletePackageReducer }, updatePackages: { type: 'PACKAGES_UPDATE', reducer: updatePackagesReducer }, registerError: { type: 'PACKAGES_REDUCER_ERROR', reducer: errorReducer }, + resetPendingAndError: { type: 'PACKAGES_REDUCER_RESET_ERROR_PENDING', reducer: resetPendingAndErrorReducer }, isFetching: { multiType: fetchableTypes, reducer: pendingReducer } }); diff --git a/src/store/rootEpic.js b/src/store/rootEpic.js index d947da39b..cf0ecd58f 100644 --- a/src/store/rootEpic.js +++ b/src/store/rootEpic.js @@ -5,6 +5,7 @@ import { combineEpics } from 'redux-observable'; // Epics import { epics as appEpics } from './reducers/appReducer'; import { epics as devicesEpics } from './reducers/devicesReducer'; +import { epics as deploymentsEpics } from './reducers/deploymentsReducer'; import { epics as rulesEpics } from './reducers/rulesReducer'; import { epics as packagesEpics } from './reducers/packagesReducer'; import { epics as simulationEpics } from './reducers/deviceSimulationReducer'; @@ -12,6 +13,7 @@ import { epics as simulationEpics } from './reducers/deviceSimulationReducer'; // Extract the epic function from each property object const epics = [ ...appEpics.getEpics(), + ...deploymentsEpics.getEpics(), ...devicesEpics.getEpics(), ...packagesEpics.getEpics(), ...rulesEpics.getEpics(), diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js index 70ce522e9..083b7ec2b 100644 --- a/src/store/rootReducer.js +++ b/src/store/rootReducer.js @@ -4,13 +4,15 @@ import { combineReducers } from 'redux'; // Reducers import { reducer as appReducer } from './reducers/appReducer'; -import { reducer as simulationReducer } from './reducers/deviceSimulationReducer'; +import { reducer as deploymentsReducer } from './reducers/deploymentsReducer'; import { reducer as devicesReducer } from './reducers/devicesReducer'; -import { reducer as rulesReducer } from './reducers/rulesReducer'; import { reducer as packagesReducer } from './reducers/packagesReducer'; +import { reducer as rulesReducer } from './reducers/rulesReducer'; +import { reducer as simulationReducer } from './reducers/deviceSimulationReducer'; const rootReducer = combineReducers({ ...appReducer, + ...deploymentsReducer, ...devicesReducer, ...packagesReducer, ...rulesReducer, diff --git a/src/store/utilities.js b/src/store/utilities.js index c301092b4..a66e84794 100644 --- a/src/store/utilities.js +++ b/src/store/utilities.js @@ -175,6 +175,11 @@ export const setError = (type, error) => ({ errors: { [type]: { $set: error } } }); +export const resetPendingAndErrorReducer = (state, { type }) => update(state, { + ...setPending(type, false), + ...setError(type) +}); + export const pendingReducer = (state, { type }) => update(state, { ...setPending(type, true), ...setError(type) From 236a3c97fd82f9a5b23f3d19651f1fc865f7b0b6 Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 14 Sep 2018 11:47:29 -0700 Subject: [PATCH 12/25] Merge Master into Edge Feature Branch (#1100) * Merge Master into Feature branch for Edge * nits --- public/locales/en/translations.json | 9 ++-- .../pages/dashboard/dashboard.container.js | 6 ++- src/components/pages/dashboard/dashboard.js | 46 ++++++++++++----- .../panels/analytics/analyticsPanel.js | 8 ++- .../panels/analytics/analyticsPanel.scss | 5 ++ .../panels/overview/overviewPanel.scss | 14 ++--- .../panels/telemetry/telemetryPanel.js | 8 ++- .../panels/telemetry/telemetryPanel.scss | 5 ++ src/components/pages/devices/devices.js | 29 ++++++----- .../deviceDetails/deviceDetails.container.js | 5 +- .../flyouts/deviceDetails/deviceDetails.js | 16 +++++- .../flyouts/deviceDetails/deviceDetails.scss | 4 +- .../maintenance/jobDetails/jobDetails.js | 30 +++++++---- .../maintenance/ruleDetails/ruleDetails.js | 51 +++++++++++-------- .../pages/maintenance/summary/summary.js | 31 +++++++---- src/components/pages/rules/rules.js | 21 +++++--- .../shared/contextMenu/contextMenu.scss | 9 ++++ .../shared/contextMenu/contextMenuAlign.js | 11 ++++ src/components/shared/contextMenu/index.js | 1 + src/components/shared/forms/hyperlink.js | 26 ++++++++++ src/components/shared/forms/index.js | 1 + .../shared/forms/styles/hyperlink.scss | 33 ++++++++++++ src/components/shared/pageStats/README.md | 2 +- .../deviceGroupDropdown.scss | 1 - src/services/models/telemetryModels.js | 4 ++ src/services/telemetryService.js | 9 +++- src/store/reducers/appReducer.js | 27 ++++++++-- src/styles/_themes.scss | 16 ++++++ src/styles/_variables.scss | 12 +++++ 29 files changed, 339 insertions(+), 101 deletions(-) create mode 100644 src/components/shared/contextMenu/contextMenuAlign.js create mode 100644 src/components/shared/forms/hyperlink.js create mode 100644 src/components/shared/forms/styles/hyperlink.scss diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 5e35b59f5..b22211db7 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -140,7 +140,8 @@ "deviceTypeAlerts": "Alert by device type", "criticalAlerts": "Critical alerts", "currentWindow": "Currently", - "previousWindow": "Previously" + "previousWindow": "Previously", + "exploreTimeSeries": "Explore in Time Series Insights" }, "map": { "header": "Device locations", @@ -157,7 +158,8 @@ "notConnected": "Offline" }, "telemetry": { - "header": "Telemetry" + "header": "Telemetry", + "exploreTimeSeries": "Explore in Time Series Insights" } } }, @@ -224,7 +226,8 @@ "noneExist": "No tags found for this device." }, "telemetry": { - "title": "Telemetry" + "title": "Telemetry", + "exploreTimeSeries": "Explore in Time Series Insights" } }, "delete": { diff --git a/src/components/pages/dashboard/dashboard.container.js b/src/components/pages/dashboard/dashboard.container.js index 999c0ef67..fea4b7978 100644 --- a/src/components/pages/dashboard/dashboard.container.js +++ b/src/components/pages/dashboard/dashboard.container.js @@ -11,7 +11,8 @@ import { getDeviceGroups, getDeviceGroupError, getTheme, - getTimeInterval + getTimeInterval, + getTimeSeriesExplorerUrl } from 'store/reducers/appReducer'; import { epics as rulesEpics, @@ -43,7 +44,8 @@ const mapStateToProps = state => ({ rulesError: getRulesError(state), rulesIsPending: getRulesPendingStatus(state), theme: getTheme(state), - timeInterval: getTimeInterval(state) + timeInterval: getTimeInterval(state), + timeSeriesExplorerUrl: getTimeSeriesExplorerUrl(state) }); // Wrap the dispatch method diff --git a/src/components/pages/dashboard/dashboard.js b/src/components/pages/dashboard/dashboard.js index 78b32c7e4..439a6a72a 100644 --- a/src/components/pages/dashboard/dashboard.js +++ b/src/components/pages/dashboard/dashboard.js @@ -6,6 +6,7 @@ import moment from 'moment'; import Config from 'app.config'; import { TelemetryService } from 'services'; +import { permissions } from 'services/models'; import { compareByProperty, getIntervalParams, retryHandler } from 'utilities'; import { Grid, Cell } from './grid'; import { PanelErrorBoundary } from './panel'; @@ -22,7 +23,13 @@ import { transformTelemetryResponse, chartColorObjects } from './panels'; -import { ContextMenu, PageContent, RefreshBar } from 'components/shared'; +import { + ContextMenu, + ContextMenuAlign, + PageContent, + Protected, + RefreshBar +} from 'components/shared'; import './dashboard.css'; @@ -268,6 +275,7 @@ export class Dashboard extends Component { const { theme, timeInterval, + timeSeriesExplorerUrl, azureMapsKey, azureMapsKeyError, @@ -318,6 +326,12 @@ export class Dashboard extends Component { ? deviceIds.length - onlineDeviceCount : undefined; + // Add parameters to Time Series Insights Url + const timeSeriesParamUrl = + timeSeriesExplorerUrl + ? timeSeriesExplorerUrl + '&relativeMillis=1800000&timeSeriesDefinitions=[{"name":"Devices","splitBy":"iothub-connection-device-id"}]' + : undefined; + // Add the alert rule name to the list of top alerts const topAlertsWithName = topAlerts.map(alert => ({ ...alert, @@ -343,17 +357,23 @@ export class Dashboard extends Component { return [ - - - - + + + + + + + + + + , @@ -391,6 +411,7 @@ export class Dashboard extends Component { @@ -102,6 +102,10 @@ export class AnalyticsPanel extends Component { { !showOverlay && isPending && } + { + timeSeriesExplorerUrl && + {t('dashboard.panels.analytics.exploreTimeSeries')} + }
{t('dashboard.panels.analytics.topRule')}
diff --git a/src/components/pages/dashboard/panels/analytics/analyticsPanel.scss b/src/components/pages/dashboard/panels/analytics/analyticsPanel.scss index 56d4cd095..a7bb11a29 100644 --- a/src/components/pages/dashboard/panels/analytics/analyticsPanel.scss +++ b/src/components/pages/dashboard/panels/analytics/analyticsPanel.scss @@ -39,6 +39,11 @@ justify-content: center; } + .time-series-explorer { + display: block; + @include rem-fallback(padding, 0px, 0px, 10px, 0px); + } + @include themify($themes) { // Overrides of the TSIChart Lib text { fill: themed('colorContentText'); } diff --git a/src/components/pages/dashboard/panels/overview/overviewPanel.scss b/src/components/pages/dashboard/panels/overview/overviewPanel.scss index 9d228a250..691606de6 100644 --- a/src/components/pages/dashboard/panels/overview/overviewPanel.scss +++ b/src/components/pages/dashboard/panels/overview/overviewPanel.scss @@ -10,7 +10,7 @@ padding: 0 !important; .stat-header { - padding: 10px; + @include rem-fallback(padding, 10px); font-family: $fontSelawikSemibold; @include rem-font-size(40px); } @@ -19,7 +19,9 @@ display: flex; flex-flow: column nowrap; flex-shrink: 0; - padding: 0px 10px 20px 10px; + @include rem-fallback(margin, 0px, 0px, 0px, 0px); + @include rem-fallback(padding, 0px, 10px, 20px, 10px); + align-items: flex-start; .stat-cell { display: flex; @@ -27,7 +29,7 @@ flex-shrink: 0; flex-grow: 1; align-items: flex-end; - padding: 10px 10px 10px 0px; + @include rem-fallback(padding, 6px, 6px, 6px, 0px); .stat-value { display: flex; @@ -48,8 +50,8 @@ .severity-icon { flex-shrink: 0; - margin-left: 8px; - margin-bottom: 3px; + @include rem-fallback(margin-left, 8px); + @include rem-fallback(margin-bottom, 3px); @include square-px-rem(10px); svg { @include square-px-rem(10px); } @@ -57,7 +59,7 @@ } .stat-label { - padding-left: 8px; + @include rem-fallback(padding-left, 8px); @include rem-font-size(14px); } } diff --git a/src/components/pages/dashboard/panels/telemetry/telemetryPanel.js b/src/components/pages/dashboard/panels/telemetry/telemetryPanel.js index 79f494d1a..72fb2c216 100644 --- a/src/components/pages/dashboard/panels/telemetry/telemetryPanel.js +++ b/src/components/pages/dashboard/panels/telemetry/telemetryPanel.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import 'tsiclient'; -import { AjaxError, Indicator } from 'components/shared'; +import { AjaxError, Hyperlink, Indicator } from 'components/shared'; import { Panel, PanelContent, @@ -20,7 +20,7 @@ import './telemetryPanel.css'; export class TelemetryPanel extends Component { render() { - const { t, isPending, telemetry, lastRefreshed, theme, colors, error } = this.props; + const { t, isPending, telemetry, lastRefreshed, theme, colors, error, timeSeriesExplorerUrl } = this.props; const showOverlay = isPending && !lastRefreshed; return ( @@ -29,6 +29,10 @@ export class TelemetryPanel extends Component { { !showOverlay && isPending && } + { + timeSeriesExplorerUrl && + {t('dashboard.panels.telemetry.exploreTimeSeries')} + } { !showOverlay && Object.keys(telemetry).length === 0 diff --git a/src/components/pages/dashboard/panels/telemetry/telemetryPanel.scss b/src/components/pages/dashboard/panels/telemetry/telemetryPanel.scss index 404f680b8..2edffcecf 100644 --- a/src/components/pages/dashboard/panels/telemetry/telemetryPanel.scss +++ b/src/components/pages/dashboard/panels/telemetry/telemetryPanel.scss @@ -7,3 +7,8 @@ display: flex; flex-flow: column nowrap; } + +.time-series-explorer { + display: block; + @include rem-fallback(padding, 0px, 0px, 10px, 0px); +} diff --git a/src/components/pages/devices/devices.js b/src/components/pages/devices/devices.js index 0095c4024..4357490e8 100644 --- a/src/components/pages/devices/devices.js +++ b/src/components/pages/devices/devices.js @@ -10,6 +10,7 @@ import { AjaxError, Btn, ContextMenu, + ContextMenuAlign, PageContent, Protected, RefreshBar, @@ -76,18 +77,22 @@ export class Devices extends Component { return [ - - - {this.state.contextBtns} - - {t('devices.flyouts.SIMManagement.title')} - - - {t('devices.flyouts.new.contextMenuName')} - - - - + + + + + + + + + { this.state.contextBtns } + + {t('devices.flyouts.SIMManagement.title')} + + + {t('devices.flyouts.new.contextMenuName')} + + , diff --git a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.container.js b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.container.js index b192f1b5d..7a3f6e00f 100644 --- a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.container.js +++ b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.container.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { translate } from 'react-i18next'; import { DeviceDetails } from './deviceDetails'; -import { getTheme, getDeviceGroups } from 'store/reducers/appReducer'; +import { getTheme, getDeviceGroups, getTimeSeriesExplorerUrl } from 'store/reducers/appReducer'; import { epics as ruleEpics, getEntities as getRulesEntities, @@ -17,7 +17,8 @@ const mapStateToProps = state => ({ rules: getRulesEntities(state), rulesLastUpdated: getRulesLastUpdated(state), deviceGroups: getDeviceGroups(state), - theme: getTheme(state) + theme: getTheme(state), + timeSeriesExplorerUrl: getTimeSeriesExplorerUrl(state) }); // Wrap the dispatch method diff --git a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js index 4bf35d566..b88530221 100644 --- a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js +++ b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js @@ -19,6 +19,7 @@ import { Btn, BtnToolbar, ErrorMsg, + Hyperlink, PropertyGrid as Grid, PropertyGridBody as GridBody, PropertyGridHeader as GridHeader, @@ -159,7 +160,7 @@ export class DeviceDetails extends Component { } render() { - const { t, onClose, device, theme } = this.props; + const { t, onClose, device, theme, timeSeriesExplorerUrl } = this.props; const { telemetry, lastMessage } = this.state; const lastMessageTime = (lastMessage || {}).time; const isPending = this.state.isAlertsPending && this.props.isRulesPending; @@ -174,6 +175,13 @@ export class DeviceDetails extends Component { const tags = Object.entries(device.tags || {}); const properties = Object.entries(device.properties || {}); + // Add parameters to Time Series Insights Url + const timeSeriesParamUrl = + timeSeriesExplorerUrl + ? timeSeriesExplorerUrl + + `&relativeMillis=1800000&timeSeriesDefinitions=[{"name":"${device.id}","measureName":"${Object.keys(telemetry).sort()[0]}","predicate":"'${device.id}'"}]` + : undefined; + return ( @@ -210,7 +218,11 @@ export class DeviceDetails extends Component { {t('devices.flyouts.details.telemetry.title')} - + { + timeSeriesExplorerUrl && + {t('devices.flyouts.details.telemetry.exploreTimeSeries')} + } + diff --git a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.scss b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.scss index f8c67d1c4..20b4eb214 100644 --- a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.scss +++ b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.scss @@ -36,9 +36,11 @@ .raw-message-button { text-align: left; padding: 0; - margin-right: 10px; + @include rem-fallback(margin-right, 10px); } + .time-series-explorer { display: block; } + @include themify($themes) { .device-details-header { .device-icon svg { fill: themed('colorContentTextDim'); } diff --git a/src/components/pages/maintenance/jobDetails/jobDetails.js b/src/components/pages/maintenance/jobDetails/jobDetails.js index 6114c7014..090c8d96e 100644 --- a/src/components/pages/maintenance/jobDetails/jobDetails.js +++ b/src/components/pages/maintenance/jobDetails/jobDetails.js @@ -3,7 +3,13 @@ import React, { Component } from 'react'; import Config from 'app.config'; -import { AjaxError, PageContent, ContextMenu, RefreshBar } from 'components/shared'; +import { + AjaxError, + ContextMenu, + ContextMenuAlign, + PageContent, + RefreshBar +} from 'components/shared'; import { DevicesGrid } from 'components/pages/devices/devicesGrid'; import { JobGrid, JobStatusGrid } from 'components/pages/maintenance/grids'; import { TimeIntervalDropdown } from 'components/shell/timeIntervalDropdown'; @@ -114,16 +120,18 @@ export class JobDetails extends Component { return [ - {this.state.contextBtns} - - + + {this.state.contextBtns} + + + ,

{selectedJob ? selectedJob.jobId : ""}

diff --git a/src/components/pages/maintenance/ruleDetails/ruleDetails.js b/src/components/pages/maintenance/ruleDetails/ruleDetails.js index 912431dd1..fa1ca7c2a 100644 --- a/src/components/pages/maintenance/ruleDetails/ruleDetails.js +++ b/src/components/pages/maintenance/ruleDetails/ruleDetails.js @@ -11,6 +11,7 @@ import { AjaxError, Btn, ContextMenu, + ContextMenuAlign, Indicator, PageContent, Protected, @@ -281,28 +282,34 @@ export class RuleDetails extends Component { const { counts = {} } = selectedAlert; return [ - - { - this.state.updatingAlertStatus && -
- -
- } - { - this.state.ruleContextBtns - || this.state.alertContextBtns - || this.state.deviceContextBtns - } - - - + + + + + + + + { + this.state.updatingAlertStatus && +
+ +
+ } + { + this.state.ruleContextBtns + || this.state.alertContextBtns + || this.state.deviceContextBtns + } + + +
, { diff --git a/src/components/pages/maintenance/summary/summary.js b/src/components/pages/maintenance/summary/summary.js index d602834ee..a03bbb16c 100644 --- a/src/components/pages/maintenance/summary/summary.js +++ b/src/components/pages/maintenance/summary/summary.js @@ -5,6 +5,7 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { NavLink } from 'react-router-dom'; +import { permissions } from 'services/models'; import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/shell/deviceGroupDropdown'; import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/shell/manageDeviceGroupsBtn'; import { TimeIntervalDropdown } from 'components/shell/timeIntervalDropdown'; @@ -13,8 +14,10 @@ import { Jobs } from './jobs'; import { PageContent, ContextMenu, + ContextMenuAlign, RefreshBar, PageTitle, + Protected, StatSection, StatGroup, StatProperty @@ -37,17 +40,23 @@ export const Summary = ({ ...props }) => [ - - - - + + + + + + + + + + , diff --git a/src/components/pages/rules/rules.js b/src/components/pages/rules/rules.js index b97f64967..3a85e947a 100644 --- a/src/components/pages/rules/rules.js +++ b/src/components/pages/rules/rules.js @@ -9,6 +9,7 @@ import { AjaxError, Btn, ContextMenu, + ContextMenuAlign, PageContent, Protected, RefreshBar, @@ -87,13 +88,19 @@ export class Rules extends Component { }; return [ - - - {this.state.contextBtns} - - {t('rules.flyouts.newRule')} - - + + + + + + + + + {this.state.contextBtns} + + {t('rules.flyouts.newRule')} + + , diff --git a/src/components/shared/contextMenu/contextMenu.scss b/src/components/shared/contextMenu/contextMenu.scss index 62d71bd1f..b90464cf4 100644 --- a/src/components/shared/contextMenu/contextMenu.scss +++ b/src/components/shared/contextMenu/contextMenu.scss @@ -12,6 +12,15 @@ $headerHeight: 48px; justify-content: flex-end; // Move buttons to the right of the context menu @include rem-fallback(height, $headerHeight); + .context-menu-align-container { + display: flex; + flex-grow: 1; + + &.left { justify-content: flex-start; } + + &.right { justify-content: flex-end; } + } + .last-updated-container { margin: 0; align-items: center; diff --git a/src/components/shared/contextMenu/contextMenuAlign.js b/src/components/shared/contextMenu/contextMenuAlign.js new file mode 100644 index 000000000..c72b28d7d --- /dev/null +++ b/src/components/shared/contextMenu/contextMenuAlign.js @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; + +import { joinClasses } from 'utilities'; + +import './contextMenu.css'; + +export const ContextMenuAlign = ({ children, className, left }) => ( +
{ children }
+); diff --git a/src/components/shared/contextMenu/index.js b/src/components/shared/contextMenu/index.js index 98ba9a2ce..d04954062 100644 --- a/src/components/shared/contextMenu/index.js +++ b/src/components/shared/contextMenu/index.js @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. export * from './contextMenu'; +export * from './contextMenuAlign'; export * from './searchInput/searchInput'; diff --git a/src/components/shared/forms/hyperlink.js b/src/components/shared/forms/hyperlink.js new file mode 100644 index 000000000..931966097 --- /dev/null +++ b/src/components/shared/forms/hyperlink.js @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { joinClasses } from 'utilities'; + +import './styles/hyperlink.css'; + +export const Hyperlink = (props) => { + const { children, className, href } = props; + + if (href == null) return null; + return ( + + {children} + + ); +}; + +Hyperlink.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + href: PropTypes.string, + target: PropTypes.string +}; diff --git a/src/components/shared/forms/index.js b/src/components/shared/forms/index.js index ec17d5e4a..38f6ead61 100644 --- a/src/components/shared/forms/index.js +++ b/src/components/shared/forms/index.js @@ -11,6 +11,7 @@ export * from './formControl'; export * from './formGroup'; export * from './formLabel'; export * from './formSection'; +export * from './hyperlink'; export * from './radio'; export * from './sectionDesc'; export * from './sectionHeader'; diff --git a/src/components/shared/forms/styles/hyperlink.scss b/src/components/shared/forms/styles/hyperlink.scss new file mode 100644 index 000000000..7ebba7f6f --- /dev/null +++ b/src/components/shared/forms/styles/hyperlink.scss @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/themes'; +@import 'src/styles/mixins'; + +.hyperlink { + cursor: pointer; + text-decoration: none; + @include rem-font-size(14px); + + &:disabled { cursor: auto; } + + @include themify($themes) { + color: themed('colorHyperlinkText'); + + &:focus { + color: themed('colorHyperlinkTextFocus'); + outline: 1px solid themed('colorHyperlinkOutlineFocus'); + outline-offset: 6px; + } + + &:hover { + color: themed('colorHyperlinkTextHover'); + text-decoration: underline; + } + + &:disabled { color: themed('colorHyperlinkTextDisabled'); } + + &:visited { color: themed('colorHyperlinkText'); } + + &:visited:hover { color: themed('colorHyperlinkTextHover'); } + } +} diff --git a/src/components/shared/pageStats/README.md b/src/components/shared/pageStats/README.md index d43de82b2..e7718aebe 100644 --- a/src/components/shared/pageStats/README.md +++ b/src/components/shared/pageStats/README.md @@ -20,7 +20,7 @@ A presentational component containing number value, label, and an optional svg a ```html - + ``` diff --git a/src/components/shell/deviceGroupDropdown/deviceGroupDropdown.scss b/src/components/shell/deviceGroupDropdown/deviceGroupDropdown.scss index 1984bb415..add59e9dc 100644 --- a/src/components/shell/deviceGroupDropdown/deviceGroupDropdown.scss +++ b/src/components/shell/deviceGroupDropdown/deviceGroupDropdown.scss @@ -4,7 +4,6 @@ @import 'src/styles/mixins'; .device-group-dropdown { - margin-right: auto; /* Push to the right of the other context menu buttons*/ @include rem-fallback(width, 200px); .Select-menu-outer { diff --git a/src/services/models/telemetryModels.js b/src/services/models/telemetryModels.js index 0c7bd795b..d84a76859 100644 --- a/src/services/models/telemetryModels.js +++ b/src/services/models/telemetryModels.js @@ -123,6 +123,10 @@ export const toMessagesModel = (response = {}) => getItems(response) 'time': 'time' })); +export const toStatusModel = (response = {}) => camelCaseReshape(response, { + 'properties': 'properties' +}); + export const toEditRuleRequestModel = ({ id, name, diff --git a/src/services/telemetryService.js b/src/services/telemetryService.js index d0fca62ec..23e7ca345 100644 --- a/src/services/telemetryService.js +++ b/src/services/telemetryService.js @@ -10,7 +10,8 @@ import { toAlertsModel, toMessagesModel, toRuleModel, - toRulesModel + toRulesModel, + toStatusModel } from './models'; const ENDPOINT = Config.serviceUrls.telemetry; @@ -18,6 +19,12 @@ const ENDPOINT = Config.serviceUrls.telemetry; /** Contains methods for calling the telemetry service */ export class TelemetryService { + /** Returns the status properties for the telemetry service */ + static getStatus() { + return HttpClient.get(`${ENDPOINT}status`) + .map(toStatusModel); + } + /** Returns a list of rules */ static getRules(params = {}) { return HttpClient.get(`${ENDPOINT}rules?${stringify(params)}`) diff --git a/src/store/reducers/appReducer.js b/src/store/reducers/appReducer.js index 2f6c4e9cc..859e57f34 100644 --- a/src/store/reducers/appReducer.js +++ b/src/store/reducers/appReducer.js @@ -2,7 +2,7 @@ import 'rxjs'; import { Observable } from 'rxjs'; -import { AuthService, ConfigService, GitHubService, DiagnosticsService } from 'services'; +import { AuthService, ConfigService, GitHubService, DiagnosticsService, TelemetryService } from 'services'; import moment from 'moment'; import { schema, normalize } from 'normalizr'; import { createSelector } from 'reselect'; @@ -35,7 +35,8 @@ export const epics = createEpicScenario({ epics.actions.fetchDeviceGroups(), epics.actions.fetchLogo(), epics.actions.fetchReleaseInformation(), - epics.actions.fetchSolutionSettings() + epics.actions.fetchSolutionSettings(), + epics.actions.fetchTelemetryStatus() ] }, @@ -76,6 +77,15 @@ export const epics = createEpicScenario({ .catch(handleError(fromAction)) }, + /** Get Telemetry Status */ + fetchTelemetryStatus: { + type: 'APP_FETCH_TELEMETRY_STATUS', + epic: (fromAction) => + TelemetryService.getStatus() + .map(toActionCreator(redux.actions.updateTelemetryProperties, fromAction)) + .catch(handleError(fromAction)) + }, + /** Update solution settings */ updateDiagnosticsOptIn: { type: 'APP_UPDATE_DIAGNOSTICS_OPTOUT', @@ -174,6 +184,7 @@ const initialState = { theme: 'dark', version: undefined, releaseNotesUrl: undefined, + timeSeriesExplorerUrl: undefined, logo: svgs.contoso, name: 'companyName', isDefaultLogo: true, @@ -197,6 +208,13 @@ const updateUserReducer = (state, { payload, fromAction }) => { }); }; +const updateTelemetryPropertiesReducer = (state, { payload, fromAction }) => { + return update(state, { + timeSeriesExplorerUrl: { $set: payload.properties.tsiExplorerUrl }, + ...setPending(fromAction.type, false) + }); +}; + const updateDeviceGroupsReducer = (state, { payload, fromAction }) => { const { entities: { deviceGroups } } = normalize(payload, deviceGroupListSchema); return update(state, { @@ -259,11 +277,13 @@ const fetchableTypes = [ epics.actionTypes.fetchDeviceGroupFilters, epics.actionTypes.updateLogo, epics.actionTypes.fetchLogo, - epics.actions.fetchSolutionSettings + epics.actions.fetchSolutionSettings, + epics.actions.fetchTelemetryStatus ]; export const redux = createReducerScenario({ updateUser: { type: 'APP_USER_UPDATE', reducer: updateUserReducer }, + updateTelemetryProperties: { type: 'APP_UPDATE_TELEMETRY_STATUS', reducer: updateTelemetryPropertiesReducer }, updateDeviceGroups: { type: 'APP_DEVICE_GROUP_UPDATE', reducer: updateDeviceGroupsReducer }, deleteDeviceGroups: { type: 'APP_DEVICE_GROUP_DELETE', reducer: deleteDeviceGroupsReducer }, insertDeviceGroups: { type: 'APP_DEVICE_GROUP_INSERT', reducer: insertDeviceGroupsReducer }, @@ -286,6 +306,7 @@ export const reducer = { app: redux.getReducer(initialState) }; export const getAppReducer = state => state.app; export const getVersion = state => getAppReducer(state).version; export const getTheme = state => getAppReducer(state).theme; +export const getTimeSeriesExplorerUrl = state => getAppReducer(state).timeSeriesExplorerUrl; export const getDeviceGroupEntities = state => getAppReducer(state).deviceGroups; export const getActiveDeviceGroupId = state => getAppReducer(state).activeDeviceGroupId; export const getSettings = state => getAppReducer(state).settings; diff --git a/src/styles/_themes.scss b/src/styles/_themes.scss index 61013663f..640684fbf 100644 --- a/src/styles/_themes.scss +++ b/src/styles/_themes.scss @@ -75,6 +75,14 @@ $themes: ( colorBtnPrimarySvgFillDisabled: $colorSmoke, // Btn - END + // Hyperlink - START + colorHyperlinkText: $colorDarkLinkRest, + colorHyperlinkTextHover: $colorDarkLinkHover, + colorHyperlinkTextFocus: $colorDarkLinkFocus, + colorHyperlinkOutlineFocus: $colorDarkLinkBorder, + colorHyperlinkTextDisabled: $colorDarkLinkDisabled, + // Hyperlink - END + // Page Content Colors - START colorContentBackground: $colorNoir, colorPageContentBackground: $colorNoir, @@ -236,6 +244,14 @@ $themes: ( colorBtnPrimarySvgFillDisabled: #a6a6a6, // Btn - END + // Hyperlink - START + colorHyperlinkText: $colorLightLinkRest, + colorHyperlinkTextHover: $colorLightLinkHover, + colorHyperlinkTextFocus: $colorLightLinkFocus, + colorHyperlinkOutlineFocus: $colorLightLinkBorder, + colorHyperlinkTextDisabled: $colorLightLinkDisabled, + // Hyperlink - END + // Page Content Colors - START colorContentBackground: #f2f2f2, // Grey100 colorPageContentBackground: $colorWhite, diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index c0ece5848..2bd4615cf 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -24,6 +24,18 @@ $colorSmoke: #afb9c3; $colorStone: #6a737c; $colorWhite: #fff; +$colorDarkLinkRest: #60AAFF; +$colorDarkLinkHover: #2F7FDB; +$colorDarkLinkFocus: #8DAACB; +$colorDarkLinkBorder: #666666; +$colorDarkLinkDisabled: #666666; + +$colorLightLinkRest: #136BFB; +$colorLightLinkHover: #0053B3; +$colorLightLinkFocus: #136BFB; +$colorLightLinkBorder: #666666; +$colorLightLinkDisabled: #A6A6A6; + // Function color variables $colorAlert: #fc540a; $colorWarning: #ffee91; From aa31893f7255c19ca1e4ec681a1e04875cb14a78 Mon Sep 17 00:00:00 2001 From: Mary Ellen Chaffin Date: Mon, 17 Sep 2018 14:06:32 -0700 Subject: [PATCH 13/25] Add glimmer icon for newly added deployments (#1101) --- .../deployments/deploymentsGrid/deploymentsGridConfig.js | 5 +++-- src/store/reducers/deploymentsReducer.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js b/src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js index 20b9c7f8c..1b8865be8 100644 --- a/src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js +++ b/src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import Config from 'app.config'; -import { TimeRenderer } from 'components/shared/cellRenderers'; +import { SoftSelectLinkRenderer, TimeRenderer } from 'components/shared/cellRenderers'; import { gridValueFormatters } from 'components/shared/pcsGrid/pcsGridConfig'; const { checkForEmpty } = gridValueFormatters; @@ -11,7 +11,8 @@ export const deploymentsColumnDefs = { headerName: 'deployments.grid.name', field: 'name', sort: 'asc', - valueFormatter: ({ value }) => checkForEmpty(value) + valueFormatter: ({ value }) => checkForEmpty(value), + cellRendererFramework: SoftSelectLinkRenderer }, package: { headerName: 'deployments.grid.package', diff --git a/src/store/reducers/deploymentsReducer.js b/src/store/reducers/deploymentsReducer.js index 36f439c86..744ae4a21 100644 --- a/src/store/reducers/deploymentsReducer.js +++ b/src/store/reducers/deploymentsReducer.js @@ -62,7 +62,7 @@ const deploymentListSchema = new schema.Array(deploymentSchema); const initialState = { ...errorPendingInitialState, entities: {} }; const insertDeploymentReducer = (state, { payload, fromAction }) => { - const { entities: { deployments }, result } = normalize(payload, deploymentSchema); + const { entities: { deployments }, result } = normalize({...payload, isNew: true}, deploymentSchema); if (state.entities) { return update(state, { entities: { $merge: deployments }, From fd36538c1244ba403ab3ea66e8de68796f71326f Mon Sep 17 00:00:00 2001 From: Mary Ellen Chaffin Date: Thu, 20 Sep 2018 16:22:07 -0700 Subject: [PATCH 14/25] More context bar arrangement; also page titles (#1104) * More context bar arrangement; also page titles * format per vscode auto formatting --- public/locales/en/translations.json | 2 + .../pages/deployments/deployments.js | 23 +++-- src/components/pages/devices/devices.js | 2 + .../maintenance/jobDetails/jobDetails.js | 51 ++++++----- .../maintenance/ruleDetails/ruleDetails.js | 90 ++++++++++--------- .../maintenance/ruleDetails/ruleDetails.scss | 6 -- .../pages/maintenance/summary/summary.js | 10 +-- src/components/pages/packages/packages.js | 16 ++-- src/components/pages/rules/rules.js | 2 + src/components/shared/pcsGrid/pcsGrid.scss | 5 +- .../shared/refreshBar/refreshBar.scss | 2 +- 11 files changed, 114 insertions(+), 95 deletions(-) diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index b22211db7..2375043df 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -168,6 +168,7 @@ "lastRefreshed": "Last refreshed" }, "devices": { + "title": "Devices", "searchPlaceholder": "Search devices...", "noneFound": "No devices found.", "refresh": "Refresh", @@ -373,6 +374,7 @@ } }, "rules": { + "title": "Rules", "searchPlaceholder": "Search rules...", "severity": { "info": "Info", diff --git a/src/components/pages/deployments/deployments.js b/src/components/pages/deployments/deployments.js index 05005a434..0e3a1eb3a 100644 --- a/src/components/pages/deployments/deployments.js +++ b/src/components/pages/deployments/deployments.js @@ -7,6 +7,7 @@ import { AjaxError, Btn, ContextMenu, + ContextMenuAlign, PageContent, Protected, RefreshBar, @@ -67,17 +68,21 @@ export class Deployments extends Component { return [ - - - {this.state.contextBtns} - - {t('deployments.flyouts.new.contextMenuName')} - - - - + + + + + + + + {this.state.contextBtns} + + {t('deployments.flyouts.new.contextMenuName')} + + , + {!!error && } {!error && } diff --git a/src/components/pages/devices/devices.js b/src/components/pages/devices/devices.js index 4357490e8..e8294a87e 100644 --- a/src/components/pages/devices/devices.js +++ b/src/components/pages/devices/devices.js @@ -12,6 +12,7 @@ import { ContextMenu, ContextMenuAlign, PageContent, + PageTitle, Protected, RefreshBar, SearchInput @@ -96,6 +97,7 @@ export class Devices extends Component { , + {!!error && } {!error && } {newDeviceFlyoutOpen && } diff --git a/src/components/pages/maintenance/jobDetails/jobDetails.js b/src/components/pages/maintenance/jobDetails/jobDetails.js index 090c8d96e..dd2297af5 100644 --- a/src/components/pages/maintenance/jobDetails/jobDetails.js +++ b/src/components/pages/maintenance/jobDetails/jobDetails.js @@ -8,6 +8,7 @@ import { ContextMenu, ContextMenuAlign, PageContent, + PageTitle, RefreshBar } from 'components/shared'; import { DevicesGrid } from 'components/pages/devices/devicesGrid'; @@ -46,10 +47,10 @@ export class JobDetails extends Component { // A long term fix would be to normalize the job data in maintenance.js (similar to how Telemetry is handled there). // When/if that happens, remove all use of refreshPending in the local state of this component. if (( - nextProps.match.params.id !== (this.state.selectedJob || {}).jobId - || this.state.timeIntervalChangePending - || this.state.refreshPending - ) && nextProps.jobs.length) { + nextProps.match.params.id !== (this.state.selectedJob || {}).jobId + || this.state.timeIntervalChangePending + || this.state.refreshPending + ) && nextProps.jobs.length) { const selectedJob = nextProps.jobs.filter(({ jobId }) => jobId === nextProps.match.params.id)[0]; this.setState({ selectedJob, refreshPending: false, timeIntervalChangePending: false }, () => this.refreshJobStatus()); } @@ -126,31 +127,33 @@ export class JobDetails extends Component { onChange={this.onTimeIntervalChange} value={timeInterval} t={t} /> - , -

{selectedJob ? selectedJob.jobId : ""}

+ + { !error - ?
- - {!isPending && !selectedJob && t('maintenance.noData')} - {selectedJob && } - {

{t('maintenance.devices')}

} - { - this.state.selectedDevices - ? - : t('maintenance.noOccurrenceSelected') - } + ? +
+ + {!isPending && !selectedJob && t('maintenance.noData')} + {selectedJob && } + {

{t('maintenance.devices')}

} + { + this.state.selectedDevices + ? + + : t('maintenance.noOccurrenceSelected') + }
: } diff --git a/src/components/pages/maintenance/ruleDetails/ruleDetails.js b/src/components/pages/maintenance/ruleDetails/ruleDetails.js index fa1ca7c2a..38ea1d010 100644 --- a/src/components/pages/maintenance/ruleDetails/ruleDetails.js +++ b/src/components/pages/maintenance/ruleDetails/ruleDetails.js @@ -14,6 +14,7 @@ import { ContextMenuAlign, Indicator, PageContent, + PageTitle, Protected, RefreshBar } from 'components/shared'; @@ -91,10 +92,10 @@ export class RuleDetails extends Component { ) .flatMap(transformTelemetryResponse(() => this.state.telemetry)) .map(telemetry => ({ telemetry, telemetryIsPending: false })) - } else { - return Observable.empty(); - } + } else { + return Observable.empty(); } + } ) .subscribe( telemetryState => this.setState( @@ -138,12 +139,12 @@ export class RuleDetails extends Component { const devices = deviceIds.map(deviceId => deviceObjects[deviceId]); const deviceIdString = deviceIds.sort().join(idDelimiter); this.setState({ - deviceIds: deviceIdString, - devices, - occurrences, - selectedAlert, - selectedRule - }, + deviceIds: deviceIdString, + devices, + occurrences, + selectedAlert, + selectedRule + }, () => this.restartTelemetry$.next(deviceIdString) ); } @@ -204,22 +205,22 @@ export class RuleDetails extends Component { const alertContextBtns = selectedRows.length > 0 ? [ - - - Close - - , - - - Acknowledge - - , - - - Delete - - - ] + + + Close + + , + + + Acknowledge + + , + + + Delete + + + ] : null; this.setState({ selectedAlerts: selectedRows, @@ -246,7 +247,7 @@ export class RuleDetails extends Component { deviceContextBtns: undefined }, this.props.refreshData); - render () { + render() { const { error, isPending, @@ -304,19 +305,20 @@ export class RuleDetails extends Component { onChange={onTimeIntervalChange} value={timeInterval} t={t} /> - , - { - !this.props.error - ?
+ + + { + !this.props.error + ? +
-

{alertName}

{t('maintenance.total')}
@@ -357,9 +359,9 @@ export class RuleDetails extends Component {
- { t('maintenance.ruleDetailsDesc') } + {t('maintenance.ruleDetailsDesc')}
-

{ t('maintenance.ruleDetail') }

+

{t('maintenance.ruleDetail')}

-

{ t('maintenance.alertOccurrences') }

+

{t('maintenance.alertOccurrences')}

-

{ t('maintenance.relatedInfo') }

+

{t('maintenance.relatedInfo')}

+ onClick={this.setTab(tabIds.all)}>{t('maintenance.all')} + onClick={this.setTab(tabIds.devices)}>{t('maintenance.devices')} + onClick={this.setTab(tabIds.telemetry)}>{t('maintenance.telemetry')}
{ (selectedTab === tabIds.all || selectedTab === tabIds.devices) && @@ -408,8 +410,8 @@ export class RuleDetails extends Component { ] }
- : - } + : + } ]; } diff --git a/src/components/pages/maintenance/ruleDetails/ruleDetails.scss b/src/components/pages/maintenance/ruleDetails/ruleDetails.scss index 474bda3b3..8d508d55b 100644 --- a/src/components/pages/maintenance/ruleDetails/ruleDetails.scss +++ b/src/components/pages/maintenance/ruleDetails/ruleDetails.scss @@ -14,12 +14,6 @@ .rule-details-container { overflow-y: scroll; // Scroll y-axis to avoid x-axis scroll on grids - .rule-maintenance-header { - font-weight: 700; - @include rem-fallback(margin, 0px, 10px, 10px, 0px); - @include rem-font-size(42px); - } - .rule-stat-container { display: flex; flex-flow: row wrap; diff --git a/src/components/pages/maintenance/summary/summary.js b/src/components/pages/maintenance/summary/summary.js index a03bbb16c..9af4dad85 100644 --- a/src/components/pages/maintenance/summary/summary.js +++ b/src/components/pages/maintenance/summary/summary.js @@ -51,14 +51,14 @@ export const Summary = ({ onChange={onTimeIntervalChange} value={timeInterval} t={props.t} /> - , + diff --git a/src/components/pages/packages/packages.js b/src/components/pages/packages/packages.js index 6ba22de98..b98276449 100644 --- a/src/components/pages/packages/packages.js +++ b/src/components/pages/packages/packages.js @@ -7,6 +7,7 @@ import { AjaxError, Btn, ContextMenu, + ContextMenuAlign, PageContent, Protected, RefreshBar, @@ -63,13 +64,18 @@ export class Packages extends Component { return [ - - {this.state.contextBtns} - - {t('packages.new')} - + + { /* Add left aligned items as needed */ } + + + {this.state.contextBtns} + + {t('packages.new')} + + , + {!!error && } {!error && } diff --git a/src/components/pages/rules/rules.js b/src/components/pages/rules/rules.js index 3a85e947a..3115e6bbd 100644 --- a/src/components/pages/rules/rules.js +++ b/src/components/pages/rules/rules.js @@ -11,6 +11,7 @@ import { ContextMenu, ContextMenuAlign, PageContent, + PageTitle, Protected, RefreshBar, SearchInput @@ -104,6 +105,7 @@ export class Rules extends Component { , + { !!error && } {!error && } {this.state.openFlyoutName === 'newRule' && } diff --git a/src/components/shared/pcsGrid/pcsGrid.scss b/src/components/shared/pcsGrid/pcsGrid.scss index a855ef76e..01a33d23f 100644 --- a/src/components/shared/pcsGrid/pcsGrid.scss +++ b/src/components/shared/pcsGrid/pcsGrid.scss @@ -7,6 +7,9 @@ $selectedRowBorderWidth: 4px; $rowHeight: 48px; $headerResizeHeight: 18px; +$pageTitleHeight: 85px; +$refreshBarHeight: 16px; + $icons-path: "~ag-grid/src/styles/icons/"; $row-height: $rowHeight; $header-height: $rowHeight; @@ -27,7 +30,7 @@ $header-background-color: transparent; $doublePadding: $baseContentPadding * 2; .pcs-grid-container.ag-theme-dark { @include ag-theme-classic($params); - height: calc(100% - #{$doublePadding}); + height: calc(100% - #{$pageTitleHeight} - #{$refreshBarHeight}); position: relative; font-family: $fontSelawik; flex-shrink: 0; diff --git a/src/components/shared/refreshBar/refreshBar.scss b/src/components/shared/refreshBar/refreshBar.scss index 2a95db99f..00230ab33 100644 --- a/src/components/shared/refreshBar/refreshBar.scss +++ b/src/components/shared/refreshBar/refreshBar.scss @@ -11,7 +11,7 @@ $rotateTime: 2s; text-transform: uppercase; font-size: 0.9em; flex-shrink: 0; - @include rem-fallback(margin-top, 13px); + margin-top: 0; .btn.refresh-btn { @include rem-fallback(padding, 0px, 5px); From 669a44cc5734a94ccee487aea0ace21fda891354 Mon Sep 17 00:00:00 2001 From: Mary Ellen Chaffin Date: Mon, 24 Sep 2018 14:47:48 -0700 Subject: [PATCH 15/25] implement new ComponentArray and change existing code to use it (#1108) --- src/components/pages/dashboard/dashboard.js | 464 +++++++++--------- .../pages/deployments/deployments.js | 49 +- .../deploymentsGrid/deploymentsGrid.js | 4 +- .../flyouts/deploymentNew/deploymentNew.js | 37 +- src/components/pages/devices/devices.js | 59 +-- .../pages/devices/devicesGrid/devicesGrid.js | 19 +- .../flyouts/deviceDetails/deviceDetails.js | 27 +- .../flyouts/deviceJobs/deviceJobMethods.js | 11 +- .../flyouts/deviceJobs/deviceJobProperties.js | 35 +- .../flyouts/deviceJobs/deviceJobTags.js | 41 +- .../devices/flyouts/deviceJobs/deviceJobs.js | 34 +- .../devices/flyouts/deviceNew/deviceNew.js | 48 +- .../maintenance/jobDetails/jobDetails.js | 81 +-- .../maintenance/ruleDetails/ruleDetails.js | 271 +++++----- .../pages/maintenance/summary/summary.js | 14 +- src/components/pages/packages/packages.js | 43 +- .../packages/packagesGrid/packagesGrid.js | 23 +- .../pages/rules/flyouts/ruleDetailsFlyout.js | 25 +- src/components/pages/rules/rules.js | 53 +- .../pages/rules/rulesGrid/rulesGrid.js | 20 +- .../shared/componentArray/componentArray.js | 3 + src/components/shared/index.js | 1 + .../components/pages/dashboard/dashboard.js | 64 +-- .../pages/pageWithFlyout/pageWithFlyout.js | 22 +- .../pageWithGrid/exampleGrid/exampleGrid.js | 11 +- .../pages/pageWithGrid/pageWithGrid.js | 23 +- 26 files changed, 779 insertions(+), 703 deletions(-) create mode 100644 src/components/shared/componentArray/componentArray.js diff --git a/src/components/pages/dashboard/dashboard.js b/src/components/pages/dashboard/dashboard.js index 439a6a72a..e0e51a38b 100644 --- a/src/components/pages/dashboard/dashboard.js +++ b/src/components/pages/dashboard/dashboard.js @@ -24,6 +24,7 @@ import { chartColorObjects } from './panels'; import { + ComponentArray, ContextMenu, ContextMenuAlign, PageContent, @@ -95,158 +96,158 @@ export class Dashboard extends Component { .map(telemetry => ({ telemetry, telemetryIsPending: false })) // Stream emits new state // Retry any retryable errors .retryWhen(retryHandler(maxRetryAttempts, retryWaitTime)); - // Telemetry stream - END - - // Analytics stream - START - - // TODO: Add device ids to params - START - const getAnalyticsStream = ({ deviceIds = [], timeInterval }) => this.panelsRefresh$ - .delay(Config.dashboardRefreshInterval) - .startWith(0) - .do(_ => this.setState({ analyticsIsPending: true })) - .flatMap(_ => { - const devices = deviceIds.length ? deviceIds.join(',') : undefined; - const [ currentIntervalParams, previousIntervalParams ] = getIntervalParams(timeInterval); - - const currentParams = { ...currentIntervalParams, devices }; - const previousParams = { ...previousIntervalParams, devices }; - - return Observable.forkJoin( - TelemetryService.getActiveAlerts(currentParams), - TelemetryService.getActiveAlerts(previousParams), - - TelemetryService.getAlerts(currentParams), - TelemetryService.getAlerts(previousParams) - ) - }).map(([ - currentActiveAlerts, - previousActiveAlerts, - - currentAlerts, - previousAlerts - ]) => { - // Process all the data out of the currentAlerts list - const currentAlertsStats = currentAlerts.reduce((acc, alert) => { - const isOpen = alert.status === Config.alertStatus.open; - const isWarning = alert.severity === Config.ruleSeverity.warning; - const isCritical = alert.severity === Config.ruleSeverity.critical; - let updatedAlertsPerDeviceId = acc.alertsPerDeviceId; - if (alert.deviceId) { - updatedAlertsPerDeviceId = { - ...updatedAlertsPerDeviceId, - [alert.deviceId]: (updatedAlertsPerDeviceId[alert.deviceId] || 0) + 1 - }; - } - return { - openWarningCount: (acc.openWarningCount || 0) + (isWarning && isOpen ? 1 : 0), - openCriticalCount: (acc.openCriticalCount || 0) + (isCritical && isOpen ? 1 : 0), - totalCriticalCount: (acc.totalCriticalCount || 0) + (isCritical ? 1 : 0), - alertsPerDeviceId: updatedAlertsPerDeviceId - }; - }, - { alertsPerDeviceId: {} } - ); - - // ================== Critical Alerts Count - START - const currentCriticalAlerts = currentAlertsStats.totalCriticalCount; - const previousCriticalAlerts = previousAlerts.reduce( - (cnt, { severity }) => severity === Config.ruleSeverity.critical ? cnt + 1 : cnt, - 0 - ); - const criticalAlertsChange = ((currentCriticalAlerts - previousCriticalAlerts) / currentCriticalAlerts * 100).toFixed(2); - // ================== Critical Alerts Count - END - - // ================== Top Alerts - START - const currentTopAlerts = currentActiveAlerts - .sort(compareByProperty('count')) - .slice(0, Config.maxTopAlerts); - - // Find the previous counts for the current top analytics - const previousTopAlertsMap = previousActiveAlerts.reduce( - (acc, { ruleId, count }) => - (ruleId in acc) - ? { ...acc, [ruleId]: count } - : acc - , - currentTopAlerts.reduce((acc, { ruleId }) => ({ ...acc, [ruleId]: 0 }), {}) - ); - - const topAlerts = currentTopAlerts.map(({ ruleId, count }) => ({ - ruleId, - count, - previousCount: previousTopAlertsMap[ruleId] || 0 - })); - // ================== Top Alerts - END - - const devicesInAlert = currentAlerts - .filter(({ status }) => status === Config.alertStatus.open) - .reduce((acc, { deviceId, severity, ruleId}) => { - return { - ...acc, - [deviceId]: { severity, ruleId } - }; - }, {}); - - return ({ - analyticsIsPending: false, - analyticsVersion: this.state.analyticsVersion + 1, - - // Analytics data - currentActiveAlerts, - topAlerts, - criticalAlertsChange, - alertsPerDeviceId: currentAlertsStats.alertsPerDeviceId, - - // Summary data - openWarningCount: currentAlertsStats.openWarningCount, - openCriticalCount: currentAlertsStats.openCriticalCount, - - // Map data - devicesInAlert - }); - }) - // Retry any retryable errors - .retryWhen(retryHandler(maxRetryAttempts, retryWaitTime)); - // Analytics stream - END - - this.subscriptions.push( - this.dashboardRefresh$ - .subscribe(() => this.setState(initialState)) - ); + // Telemetry stream - END + + // Analytics stream - START + + // TODO: Add device ids to params - START + const getAnalyticsStream = ({ deviceIds = [], timeInterval }) => this.panelsRefresh$ + .delay(Config.dashboardRefreshInterval) + .startWith(0) + .do(_ => this.setState({ analyticsIsPending: true })) + .flatMap(_ => { + const devices = deviceIds.length ? deviceIds.join(',') : undefined; + const [currentIntervalParams, previousIntervalParams] = getIntervalParams(timeInterval); + + const currentParams = { ...currentIntervalParams, devices }; + const previousParams = { ...previousIntervalParams, devices }; + + return Observable.forkJoin( + TelemetryService.getActiveAlerts(currentParams), + TelemetryService.getActiveAlerts(previousParams), + + TelemetryService.getAlerts(currentParams), + TelemetryService.getAlerts(previousParams) + ) + }).map(([ + currentActiveAlerts, + previousActiveAlerts, + + currentAlerts, + previousAlerts + ]) => { + // Process all the data out of the currentAlerts list + const currentAlertsStats = currentAlerts.reduce((acc, alert) => { + const isOpen = alert.status === Config.alertStatus.open; + const isWarning = alert.severity === Config.ruleSeverity.warning; + const isCritical = alert.severity === Config.ruleSeverity.critical; + let updatedAlertsPerDeviceId = acc.alertsPerDeviceId; + if (alert.deviceId) { + updatedAlertsPerDeviceId = { + ...updatedAlertsPerDeviceId, + [alert.deviceId]: (updatedAlertsPerDeviceId[alert.deviceId] || 0) + 1 + }; + } + return { + openWarningCount: (acc.openWarningCount || 0) + (isWarning && isOpen ? 1 : 0), + openCriticalCount: (acc.openCriticalCount || 0) + (isCritical && isOpen ? 1 : 0), + totalCriticalCount: (acc.totalCriticalCount || 0) + (isCritical ? 1 : 0), + alertsPerDeviceId: updatedAlertsPerDeviceId + }; + }, + { alertsPerDeviceId: {} } + ); - this.subscriptions.push( - this.dashboardRefresh$ - .switchMap(getTelemetryStream) - .subscribe( - telemetryState => this.setState( - { ...telemetryState, lastRefreshed: moment() }, - () => this.telemetryRefresh$.next('r') - ), - telemetryError => this.setState({ telemetryError, telemetryIsPending: false }) - ) - ); + // ================== Critical Alerts Count - START + const currentCriticalAlerts = currentAlertsStats.totalCriticalCount; + const previousCriticalAlerts = previousAlerts.reduce( + (cnt, { severity }) => severity === Config.ruleSeverity.critical ? cnt + 1 : cnt, + 0 + ); + const criticalAlertsChange = ((currentCriticalAlerts - previousCriticalAlerts) / currentCriticalAlerts * 100).toFixed(2); + // ================== Critical Alerts Count - END + + // ================== Top Alerts - START + const currentTopAlerts = currentActiveAlerts + .sort(compareByProperty('count')) + .slice(0, Config.maxTopAlerts); + + // Find the previous counts for the current top analytics + const previousTopAlertsMap = previousActiveAlerts.reduce( + (acc, { ruleId, count }) => + (ruleId in acc) + ? { ...acc, [ruleId]: count } + : acc + , + currentTopAlerts.reduce((acc, { ruleId }) => ({ ...acc, [ruleId]: 0 }), {}) + ); - this.subscriptions.push( - this.dashboardRefresh$ - .switchMap(getAnalyticsStream) - .subscribe( - analyticsState => this.setState( - { ...analyticsState, lastRefreshed: moment() }, - () => this.panelsRefresh$.next('r') - ), - analyticsError => this.setState({ analyticsError, analyticsIsPending: false }) - ) + const topAlerts = currentTopAlerts.map(({ ruleId, count }) => ({ + ruleId, + count, + previousCount: previousTopAlertsMap[ruleId] || 0 + })); + // ================== Top Alerts - END + + const devicesInAlert = currentAlerts + .filter(({ status }) => status === Config.alertStatus.open) + .reduce((acc, { deviceId, severity, ruleId }) => { + return { + ...acc, + [deviceId]: { severity, ruleId } + }; + }, {}); + + return ({ + analyticsIsPending: false, + analyticsVersion: this.state.analyticsVersion + 1, + + // Analytics data + currentActiveAlerts, + topAlerts, + criticalAlertsChange, + alertsPerDeviceId: currentAlertsStats.alertsPerDeviceId, + + // Summary data + openWarningCount: currentAlertsStats.openWarningCount, + openCriticalCount: currentAlertsStats.openCriticalCount, + + // Map data + devicesInAlert + }); + }) + // Retry any retryable errors + .retryWhen(retryHandler(maxRetryAttempts, retryWaitTime)); + // Analytics stream - END + + this.subscriptions.push( + this.dashboardRefresh$ + .subscribe(() => this.setState(initialState)) + ); + + this.subscriptions.push( + this.dashboardRefresh$ + .switchMap(getTelemetryStream) + .subscribe( + telemetryState => this.setState( + { ...telemetryState, lastRefreshed: moment() }, + () => this.telemetryRefresh$.next('r') + ), + telemetryError => this.setState({ telemetryError, telemetryIsPending: false }) + ) + ); + + this.subscriptions.push( + this.dashboardRefresh$ + .switchMap(getAnalyticsStream) + .subscribe( + analyticsState => this.setState( + { ...analyticsState, lastRefreshed: moment() }, + () => this.panelsRefresh$.next('r') + ), + analyticsError => this.setState({ analyticsError, analyticsIsPending: false }) + ) + ); + + // Start polling all panels + if (this.props.deviceLastUpdated) { + this.dashboardRefresh$.next( + refreshEvent( + Object.keys(this.props.devices || {}), + this.props.timeInterval + ) ); - - // Start polling all panels - if (this.props.deviceLastUpdated) { - this.dashboardRefresh$.next( - refreshEvent( - Object.keys(this.props.devices || {}), - this.props.timeInterval - ) - ); - } + } } componentWillUnmount() { @@ -271,7 +272,7 @@ export class Dashboard extends Component { ) ); - render () { + render() { const { theme, timeInterval, @@ -355,90 +356,93 @@ export class Dashboard extends Component { }; }, {}); - return [ - - - - - - - - - - - - , - - - - + + + + + + + + + + - - - - + + + + + - - - - - - - - - - - - { Config.showWalkthroughExamples && + + + + + + + + + + + + - + - } - - - ]; + { + Config.showWalkthroughExamples && + + + + } + + + + ); } } diff --git a/src/components/pages/deployments/deployments.js b/src/components/pages/deployments/deployments.js index 0e3a1eb3a..0d88fa42d 100644 --- a/src/components/pages/deployments/deployments.js +++ b/src/components/pages/deployments/deployments.js @@ -6,6 +6,7 @@ import { permissions } from 'services/models'; import { AjaxError, Btn, + ComponentArray, ContextMenu, ContextMenuAlign, PageContent, @@ -66,28 +67,30 @@ export class Deployments extends Component { t: t }; - return [ - - - - - - - - - {this.state.contextBtns} - - {t('deployments.flyouts.new.contextMenuName')} - - - , - - - - {!!error && } - {!error && } - {this.state.openFlyoutName === 'newDeployment' && } - - ]; + return ( + + + + + + + + + + {this.state.contextBtns} + + {t('deployments.flyouts.new.contextMenuName')} + + + + + + + {!!error && } + {!error && } + {this.state.openFlyoutName === 'newDeployment' && } + + + ); } } diff --git a/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js b/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js index 317a65703..c58937a36 100644 --- a/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js +++ b/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js @@ -50,8 +50,8 @@ export class DeploymentsGrid extends Component { } }; - return ([ + return ( - ]); + ); } } diff --git a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js index 43d516dd3..91073e73d 100644 --- a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js +++ b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js @@ -8,6 +8,7 @@ import { AjaxError, Btn, BtnToolbar, + ComponentArray, Flyout, FlyoutHeader, FlyoutTitle, @@ -226,15 +227,15 @@ export class DeploymentNew extends LinkedComponent { {t('deployments.flyouts.new.package')} {!packagesPending && !completedSuccessfully && - + } { packagesPending && @@ -269,13 +270,19 @@ export class DeploymentNew extends LinkedComponent { {/** Displays targeted devices count once device goup is selected. */ deviceFetchSuccessful && - [ - {targetedDeviceCount}, - {t('deployments.flyouts.new.targetText')}, - completedSuccessfully && , - ] + + {targetedDeviceCount} + {t('deployments.flyouts.new.targetText')} + {completedSuccessfully && } + + } + { + createIsPending && + + + {t('deployments.flyouts.new.creating')} + } - {createIsPending && [, t('deployments.flyouts.new.creating')]} {/** Displays a info message if package type selected is edge Manifest */ diff --git a/src/components/pages/devices/devices.js b/src/components/pages/devices/devices.js index e8294a87e..0844c254c 100644 --- a/src/components/pages/devices/devices.js +++ b/src/components/pages/devices/devices.js @@ -9,6 +9,7 @@ import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'compone import { AjaxError, Btn, + ComponentArray, ContextMenu, ContextMenuAlign, PageContent, @@ -76,33 +77,35 @@ export class Devices extends Component { const error = deviceGroupError || deviceError; - return [ - - - - - - - - - - { this.state.contextBtns } - - {t('devices.flyouts.SIMManagement.title')} - - - {t('devices.flyouts.new.contextMenuName')} - - - , - - - - {!!error && } - {!error && } - {newDeviceFlyoutOpen && } - {simManagementFlyoutOpen && } - - ]; + return ( + + + + + + + + + + + {this.state.contextBtns} + + {t('devices.flyouts.SIMManagement.title')} + + + {t('devices.flyouts.new.contextMenuName')} + + + + + + + {!!error && } + {!error && } + {newDeviceFlyoutOpen && } + {simManagementFlyoutOpen && } + + + ); } } diff --git a/src/components/pages/devices/devicesGrid/devicesGrid.js b/src/components/pages/devices/devicesGrid/devicesGrid.js index 4923226d6..0da27feed 100644 --- a/src/components/pages/devices/devicesGrid/devicesGrid.js +++ b/src/components/pages/devices/devicesGrid/devicesGrid.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { permissions } from 'services/models'; -import { Btn, PcsGrid, Protected } from 'components/shared'; +import { Btn, ComponentArray, PcsGrid, Protected } from 'components/shared'; import { deviceColumnDefs, defaultDeviceGridProps } from './devicesGridConfig'; import { DeviceDeleteContainer } from '../flyouts/deviceDelete'; import { DeviceJobsContainer } from '../flyouts/deviceJobs'; @@ -39,14 +39,15 @@ export class DevicesGrid extends Component { deviceColumnDefs.lastConnection ]; - this.contextBtns = [ - - {props.t('devices.flyouts.jobs.title')} - , - - {props.t('devices.flyouts.delete.title')} - - ]; + this.contextBtns = + + + {props.t('devices.flyouts.jobs.title')} + + + {props.t('devices.flyouts.delete.title')} + + ; } /** diff --git a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js index b88530221..6168f1d8a 100644 --- a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js +++ b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js @@ -18,6 +18,7 @@ import { import { Btn, BtnToolbar, + ComponentArray, ErrorMsg, Hyperlink, PropertyGrid as Grid, @@ -135,7 +136,7 @@ export class DeviceDetails extends Component { } } - toggleRawDiagnosticsMessage = () => { + toggleRawDiagnosticsMessage = () => { this.setState({ showRawMessage: !this.state.showRawMessage }); } @@ -177,10 +178,10 @@ export class DeviceDetails extends Component { // Add parameters to Time Series Insights Url const timeSeriesParamUrl = - timeSeriesExplorerUrl - ? timeSeriesExplorerUrl + + timeSeriesExplorerUrl + ? timeSeriesExplorerUrl + `&relativeMillis=1800000&timeSeriesDefinitions=[{"name":"${device.id}","measureName":"${Object.keys(telemetry).sort()[0]}","predicate":"'${device.id}'"}]` - : undefined; + : undefined; return ( @@ -288,8 +289,9 @@ export class DeviceDetails extends Component { t('devices.flyouts.details.properties.noneExist') } { - (properties.length > 0) && [ - + (properties.length > 0) && + + {t('devices.flyouts.details.properties.keyHeader')} @@ -312,14 +314,14 @@ export class DeviceDetails extends Component { }) } - , - + + {t('devices.flyouts.details.properties.copyAllProperties')} {t('devices.flyouts.details.properties.copy')} - ] + } @@ -342,10 +344,11 @@ export class DeviceDetails extends Component { {device.connected ? t('devices.flyouts.details.connected') : t('devices.flyouts.details.notConnected')} { - device.connected && [ + device.connected && + {t('devices.flyouts.details.diagnostics.lastMessage')} - { lastMessageTime ? moment(lastMessageTime).format(DEFAULT_TIME_FORMAT) : '---' } + {lastMessageTime ? moment(lastMessageTime).format(DEFAULT_TIME_FORMAT) : '---'} , {t('devices.flyouts.details.diagnostics.message')} @@ -353,7 +356,7 @@ export class DeviceDetails extends Component { {t('devices.flyouts.details.diagnostics.showMessage')} - ] + } { this.state.showRawMessage && diff --git a/src/components/pages/devices/flyouts/deviceJobs/deviceJobMethods.js b/src/components/pages/devices/flyouts/deviceJobs/deviceJobMethods.js index d5500a49c..dea3872d1 100644 --- a/src/components/pages/devices/flyouts/deviceJobs/deviceJobMethods.js +++ b/src/components/pages/devices/flyouts/deviceJobs/deviceJobMethods.js @@ -12,6 +12,7 @@ import { AjaxError, Btn, BtnToolbar, + ComponentArray, FormControl, FormGroup, FormLabel, @@ -187,16 +188,16 @@ export class DeviceJobMethods extends LinkedComponent { { this.isFirmwareUpdate() && - [ - + + {t('devices.flyouts.jobs.methods.firmwareVersion')} - , - + + {t('devices.flyouts.jobs.methods.firmwareUri')} - ] + } diff --git a/src/components/pages/devices/flyouts/deviceJobs/deviceJobProperties.js b/src/components/pages/devices/flyouts/deviceJobs/deviceJobProperties.js index 869549cad..6718f3d4a 100644 --- a/src/components/pages/devices/flyouts/deviceJobs/deviceJobProperties.js +++ b/src/components/pages/devices/flyouts/deviceJobs/deviceJobProperties.js @@ -12,6 +12,7 @@ import { AjaxError, Btn, BtnToolbar, + ComponentArray, ErrorMsg, FormControl, FormGroup, @@ -276,21 +277,25 @@ export class DeviceJobProperties extends LinkedComponent { { Object.keys(commonProperties).length > 0 && - propertyLinks.map(({ name, value, type, readOnly, edited, error }, idx) => [ - - - {name.value} - - - - - {type.value} - - , - error - ? {error} - : null - ]) + propertyLinks.map(({ name, value, type, readOnly, edited, error }, idx) => + + + + {name.value} + + + + + {type.value} + + + { + error + ? {error} + : null + } + + ) } diff --git a/src/components/pages/devices/flyouts/deviceJobs/deviceJobTags.js b/src/components/pages/devices/flyouts/deviceJobs/deviceJobTags.js index c546cc0e6..a0caed6c5 100644 --- a/src/components/pages/devices/flyouts/deviceJobs/deviceJobTags.js +++ b/src/components/pages/devices/flyouts/deviceJobs/deviceJobTags.js @@ -13,6 +13,7 @@ import { AjaxError, Btn, BtnToolbar, + ComponentArray, ErrorMsg, FormControl, FormGroup, @@ -261,24 +262,28 @@ export class DeviceJobTags extends LinkedComponent { { Object.keys(commonTags).length > 0 && - tagLinks.map(({ name, value, type, edited, error }, idx) => [ - - - - - - - - - - - - - , - error - ? {error} - : null - ]) + tagLinks.map(({ name, value, type, edited, error }, idx) => + + + + + + + + + + + + + + + { + error + ? {error} + : null + } + + ) } diff --git a/src/components/pages/devices/flyouts/deviceJobs/deviceJobs.js b/src/components/pages/devices/flyouts/deviceJobs/deviceJobs.js index 891813147..3f7964b57 100644 --- a/src/components/pages/devices/flyouts/deviceJobs/deviceJobs.js +++ b/src/components/pages/devices/flyouts/deviceJobs/deviceJobs.js @@ -5,6 +5,7 @@ import React from 'react'; import { LinkedComponent } from 'utilities'; import { permissions } from 'services/models'; import { + ComponentArray, Flyout, FlyoutHeader, FlyoutTitle, @@ -72,8 +73,9 @@ export class DeviceJobs extends LinkedComponent { {t("devices.flyouts.jobs.noDevices")} } { - devices.length > 0 && [ - + devices.length > 0 && + + {t('devices.flyouts.jobs.selectJob')} {t('devices.flyouts.jobs.tags.radioLabel')} @@ -84,17 +86,23 @@ export class DeviceJobs extends LinkedComponent { {t('devices.flyouts.jobs.properties.radioLabel')} - , - this.jobTypeLink.value === 'tags' - ? - : null, - this.jobTypeLink.value === 'methods' - ? - : null, - this.jobTypeLink.value === 'properties' - ? - : null - ] + + { + this.jobTypeLink.value === 'tags' + ? + : null + } + { + this.jobTypeLink.value === 'methods' + ? + : null + } + { + this.jobTypeLink.value === 'properties' + ? + : null + } + }
diff --git a/src/components/pages/devices/flyouts/deviceNew/deviceNew.js b/src/components/pages/devices/flyouts/deviceNew/deviceNew.js index 74c5950d9..b4c4c26d6 100644 --- a/src/components/pages/devices/flyouts/deviceNew/deviceNew.js +++ b/src/components/pages/devices/flyouts/deviceNew/deviceNew.js @@ -25,6 +25,7 @@ import { AjaxError, Btn, BtnToolbar, + ComponentArray, Flyout, FlyoutHeader, FlyoutTitle, @@ -250,9 +251,9 @@ export class DeviceNew extends LinkedComponent { ].every(link => !link.error); } - deviceTypeChange = ({ target: { value }}) => { + deviceTypeChange = ({ target: { value } }) => { this.props.logEvent(toSinglePropertyDiagnosticsModel('Devices_DeviceTypeSelect', 'DeviceType', - (value === 'true') ? Config.deviceType.simulated: Config.deviceType.physical)); + (value === 'true') ? Config.deviceType.simulated : Config.deviceType.physical)); this.formControlChange(); } @@ -370,28 +371,30 @@ export class DeviceNew extends LinkedComponent { { - isSimulatedDevice && [ - + isSimulatedDevice && + + {t('devices.flyouts.new.count.label')} - , - + + {t('devices.flyouts.new.deviceIdExample.label')}
{t('devices.flyouts.new.deviceIdExample.format', { deviceName })}
-
, - + + {t('devices.flyouts.new.deviceModel.label')} - ] +
} { - !isSimulatedDevice && [ - + !isSimulatedDevice && + + {t('devices.flyouts.new.count.label')}
{this.countLink.value}
-
, - + + {t('devices.flyouts.new.deviceId.label')} @@ -399,8 +402,8 @@ export class DeviceNew extends LinkedComponent { {t(deviceIdTypeOptions.generate.labelName)} - , - + + {t(authTypeOptions.labelName)} {t(authTypeOptions.symmetric.labelName)} @@ -408,8 +411,8 @@ export class DeviceNew extends LinkedComponent { {t(authTypeOptions.x509.labelName)} - , - + + {t(authKeyTypeOptions.labelName)} {t(authKeyTypeOptions.generate.labelName)} @@ -426,7 +429,7 @@ export class DeviceNew extends LinkedComponent {
- ] + }
@@ -451,12 +454,13 @@ export class DeviceNew extends LinkedComponent { } { - !!changesApplied && [ - , - + !!changesApplied && + + + this.onFlyoutClose('Devices_CloseClick')}>{t('devices.flyouts.new.close')} - ] + } diff --git a/src/components/pages/maintenance/jobDetails/jobDetails.js b/src/components/pages/maintenance/jobDetails/jobDetails.js index dd2297af5..f8a5157b9 100644 --- a/src/components/pages/maintenance/jobDetails/jobDetails.js +++ b/src/components/pages/maintenance/jobDetails/jobDetails.js @@ -5,6 +5,7 @@ import React, { Component } from 'react'; import Config from 'app.config'; import { AjaxError, + ComponentArray, ContextMenu, ContextMenuAlign, PageContent, @@ -119,45 +120,47 @@ export class JobDetails extends Component { t }; - return [ - - - {this.state.contextBtns} - + + + {this.state.contextBtns} + + + + + - - , - - - - { - !error - ? -
- - {!isPending && !selectedJob && t('maintenance.noData')} - {selectedJob && } - {

{t('maintenance.devices')}

} - { - this.state.selectedDevices - ? - - : t('maintenance.noOccurrenceSelected') - } -
- : - } -
- ]; + + { + !error + ? +
+ + {!isPending && !selectedJob && t('maintenance.noData')} + {selectedJob && } + {

{t('maintenance.devices')}

} + { + this.state.selectedDevices + ? + + : t('maintenance.noOccurrenceSelected') + } +
+ : + } +
+ + ); } } diff --git a/src/components/pages/maintenance/ruleDetails/ruleDetails.js b/src/components/pages/maintenance/ruleDetails/ruleDetails.js index 38ea1d010..4988c287b 100644 --- a/src/components/pages/maintenance/ruleDetails/ruleDetails.js +++ b/src/components/pages/maintenance/ruleDetails/ruleDetails.js @@ -10,6 +10,7 @@ import { RulesGrid } from 'components/pages/rules/rulesGrid'; import { AjaxError, Btn, + ComponentArray, ContextMenu, ContextMenuAlign, Indicator, @@ -204,23 +205,24 @@ export class RuleDetails extends Component { onAlertGridHardSelectChange = selectedRows => { const alertContextBtns = selectedRows.length > 0 - ? [ - + ? + + Close - , - + + Acknowledge - , - + + Delete - ] + : null; this.setState({ selectedAlerts: selectedRows, @@ -281,138 +283,139 @@ export class RuleDetails extends Component { const { selectedTab, selectedAlert = {} } = this.state; const { counts = {} } = selectedAlert; - return [ - - - - - - - - - { - this.state.updatingAlertStatus && -
- -
- } - { - this.state.ruleContextBtns - || this.state.alertContextBtns - || this.state.deviceContextBtns - } - + + + + + + + + + { + this.state.updatingAlertStatus && +
+ +
+ } + { + this.state.ruleContextBtns + || this.state.alertContextBtns + || this.state.deviceContextBtns + } + +
+
+ + -
-
, - - - - { - !this.props.error - ? -
-
-
-
-
{t('maintenance.total')}
-
{renderUndefined(counts.total)}
-
-
-
{t('maintenance.open')}
-
{renderUndefined(counts.open)}
-
-
-
{t('maintenance.acknowledged')}
-
{renderUndefined(counts.acknowledged)}
-
-
-
{t('maintenance.closed')}
-
{renderUndefined(counts.closed)}
-
-
-
{t('maintenance.lastEvent')}
-
- { - selectedAlert.lastOccurrence - ? - : Config.emptyValue - } + + { + !this.props.error + ? +
+
+
+
+
{t('maintenance.total')}
+
{renderUndefined(counts.total)}
-
-
-
{t('maintenance.severity')}
-
- { - selectedAlert.severity - ? - : Config.emptyValue - } +
+
{t('maintenance.open')}
+
{renderUndefined(counts.open)}
+
+
+
{t('maintenance.acknowledged')}
+
{renderUndefined(counts.acknowledged)}
+
+
+
{t('maintenance.closed')}
+
{renderUndefined(counts.closed)}
+
+
+
{t('maintenance.lastEvent')}
+
+ { + selectedAlert.lastOccurrence + ? + : Config.emptyValue + } +
+
+
+
{t('maintenance.severity')}
+
+ { + selectedAlert.severity + ? + : Config.emptyValue + } +
+
+ {t('maintenance.ruleDetailsDesc')} +
+

{t('maintenance.ruleDetail')}

+ + +

{t('maintenance.alertOccurrences')}

+ + +

{t('maintenance.relatedInfo')}

+
+ + + +
+ { + (selectedTab === tabIds.all || selectedTab === tabIds.devices) && + +

{t('maintenance.alertedDevices')}

+ +
+ } + { + !isPending && (selectedTab === tabIds.all || selectedTab === tabIds.telemetry) && Object.keys(this.state.telemetry).length > 0 && + +

{t('maintenance.alertedDeviceTelemetry')}

+
+ +
+
+ }
-
- {t('maintenance.ruleDetailsDesc')} -
-

{t('maintenance.ruleDetail')}

- - -

{t('maintenance.alertOccurrences')}

- - -

{t('maintenance.relatedInfo')}

-
- - - -
- { - (selectedTab === tabIds.all || selectedTab === tabIds.devices) && - [ -

{t('maintenance.alertedDevices')}

, - - ] - } - { - !isPending && (selectedTab === tabIds.all || selectedTab === tabIds.telemetry) && Object.keys(this.state.telemetry).length > 0 && - [ -

{t('maintenance.alertedDeviceTelemetry')}

, -
- -
- ] - } -
- : - } - - ]; + : + } + + + ); } } diff --git a/src/components/pages/maintenance/summary/summary.js b/src/components/pages/maintenance/summary/summary.js index 9af4dad85..b3c8be05e 100644 --- a/src/components/pages/maintenance/summary/summary.js +++ b/src/components/pages/maintenance/summary/summary.js @@ -12,10 +12,11 @@ import { TimeIntervalDropdown } from 'components/shell/timeIntervalDropdown'; import { Notifications } from './notifications'; import { Jobs } from './jobs'; import { - PageContent, + ComponentArray, ContextMenu, ContextMenuAlign, RefreshBar, + PageContent, PageTitle, Protected, StatSection, @@ -38,8 +39,9 @@ export const Summary = ({ onTimeIntervalChange, timeInterval, ...props -}) => [ - +}) => + + @@ -52,8 +54,8 @@ export const Summary = ({ value={timeInterval} t={props.t} /> - , - + +
- ]; + ; diff --git a/src/components/pages/packages/packages.js b/src/components/pages/packages/packages.js index b98276449..ffa1fb7e2 100644 --- a/src/components/pages/packages/packages.js +++ b/src/components/pages/packages/packages.js @@ -6,6 +6,7 @@ import { PackagesGrid } from './packagesGrid'; import { AjaxError, Btn, + ComponentArray, ContextMenu, ContextMenuAlign, PageContent, @@ -62,25 +63,27 @@ export class Packages extends Component { t: this.props.t }; - return [ - - - { /* Add left aligned items as needed */ } - - - {this.state.contextBtns} - - {t('packages.new')} - - - , - - - - {!!error && } - {!error && } - {this.state.openFlyoutName === 'new-Package' && } - - ]; + return ( + + + + { /* Add left aligned items as needed */} + + + {this.state.contextBtns} + + {t('packages.new')} + + + + + + + {!!error && } + {!error && } + {this.state.openFlyoutName === 'new-Package' && } + + + ); } } diff --git a/src/components/pages/packages/packagesGrid/packagesGrid.js b/src/components/pages/packages/packagesGrid/packagesGrid.js index 2c7b60ffe..e53f04364 100644 --- a/src/components/pages/packages/packagesGrid/packagesGrid.js +++ b/src/components/pages/packages/packagesGrid/packagesGrid.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { permissions } from 'services/models'; import { packagesColumnDefs, defaultPackagesGridProps } from './packagesGridConfig'; -import { Btn, PcsGrid, Protected } from 'components/shared'; +import { Btn, ComponentArray, PcsGrid, Protected } from 'components/shared'; import { isFunc, translateColumnDefs, svgs } from 'utilities'; import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig'; import { PackageDeleteContainer } from '../flyouts'; @@ -30,11 +30,12 @@ export class PackagesGrid extends Component { packagesColumnDefs.dateCreated ]; - this.contextBtns = [ - - {props.t('packages.delete')} - - ]; + this.contextBtns = + + + {props.t('packages.delete')} + + ; } getOpenFlyout = () => { @@ -99,9 +100,11 @@ export class PackagesGrid extends Component { } }; - return ([ - , - this.getOpenFlyout() - ]); + return ( + + + {this.getOpenFlyout()} + + ); } } diff --git a/src/components/pages/rules/flyouts/ruleDetailsFlyout.js b/src/components/pages/rules/flyouts/ruleDetailsFlyout.js index d7afa2f9f..409f485e9 100644 --- a/src/components/pages/rules/flyouts/ruleDetailsFlyout.js +++ b/src/components/pages/rules/flyouts/ruleDetailsFlyout.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { svgs } from 'utilities'; import { permissions, toDiagnosticsModel } from 'services/models'; -import { Btn, Protected, ProtectedError } from 'components/shared'; +import { Btn, ComponentArray, Protected, ProtectedError } from 'components/shared'; import { RuleEditorContainer } from './ruleEditor'; import { RuleViewerContainer } from './ruleViewer'; import Flyout from 'components/shared/flyout'; @@ -28,7 +28,7 @@ export class RuleDetailsFlyout extends Component { onTopXClose = () => { const { logEvent, onClose } = this.props; - if(this.state.isEditable) { + if (this.state.isEditable) { logEvent(toDiagnosticsModel('Rule_TopXCloseClick', {})); } onClose(); @@ -36,7 +36,7 @@ export class RuleDetailsFlyout extends Component { goToEditMode = () => { this.props.logEvent(toDiagnosticsModel('Rule_EditClick', {})); - this.setState({ isEditable: true }); + this.setState({ isEditable: true }); } render() { @@ -52,20 +52,21 @@ export class RuleDetailsFlyout extends Component { {!isEditable ? - [ - , - + + + {t('rules.flyouts.edit')} - ] + : - { - (hasPermission, permission) => hasPermission - ? - : - } + + { + (hasPermission, permission) => hasPermission + ? + : + } } diff --git a/src/components/pages/rules/rules.js b/src/components/pages/rules/rules.js index 3115e6bbd..79322ebad 100644 --- a/src/components/pages/rules/rules.js +++ b/src/components/pages/rules/rules.js @@ -8,6 +8,7 @@ import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'compone import { AjaxError, Btn, + ComponentArray, ContextMenu, ContextMenuAlign, PageContent, @@ -15,7 +16,7 @@ import { Protected, RefreshBar, SearchInput - } from 'components/shared'; +} from 'components/shared'; import { NewRuleFlyout } from './flyouts'; import { svgs } from 'utilities'; @@ -87,29 +88,31 @@ export class Rules extends Component { refresh: fetchRules, logEvent: this.props.logEvent }; - return [ - - - - - - - - - - {this.state.contextBtns} - - {t('rules.flyouts.newRule')} - - - , - - - - { !!error && } - {!error && } - {this.state.openFlyoutName === 'newRule' && } - - ]; + return ( + + + + + + + + + + + {this.state.contextBtns} + + {t('rules.flyouts.newRule')} + + + + + + + {!!error && } + {!error && } + {this.state.openFlyoutName === 'newRule' && } + + + ); } } diff --git a/src/components/pages/rules/rulesGrid/rulesGrid.js b/src/components/pages/rules/rulesGrid/rulesGrid.js index 5b6d03ecf..b65d689c0 100644 --- a/src/components/pages/rules/rulesGrid/rulesGrid.js +++ b/src/components/pages/rules/rulesGrid/rulesGrid.js @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import { Trans } from 'react-i18next'; import { permissions, toDiagnosticsModel } from 'services/models'; -import { Btn, PcsGrid, Protected } from 'components/shared'; +import { Btn, ComponentArray, PcsGrid, Protected } from 'components/shared'; import { rulesColumnDefs, defaultRulesGridProps } from './rulesGridConfig'; import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig'; import { isFunc, translateColumnDefs, svgs } from 'utilities'; @@ -60,13 +60,13 @@ export class RulesGrid extends Component { , edit: - + {props.t('rules.flyouts.edit')} , delete: - + Delete @@ -74,7 +74,7 @@ export class RulesGrid extends Component { }; } - componentWillReceiveProps({rowData}) { + componentWillReceiveProps({ rowData }) { const { selectedRules = [], softSelectedRule } = this.state; if (rowData && (selectedRules.length || softSelectedRule)) { let updatedSoftSelectedRule = undefined; @@ -113,7 +113,7 @@ export class RulesGrid extends Component { case 'status': return case 'delete': - return + return default: return null; } @@ -203,9 +203,11 @@ export class RulesGrid extends Component { onSoftSelectChange: rule => this.onSoftSelectChange(rule) // See above comment about closures }; - return ([ - , - this.props.suppressFlyouts ? null : this.getOpenFlyout() - ]); + return ( + + + {this.props.suppressFlyouts ? null : this.getOpenFlyout()} + + ); } } diff --git a/src/components/shared/componentArray/componentArray.js b/src/components/shared/componentArray/componentArray.js new file mode 100644 index 000000000..e004a706d --- /dev/null +++ b/src/components/shared/componentArray/componentArray.js @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +export const ComponentArray = ({ children }) => children; diff --git a/src/components/shared/index.js b/src/components/shared/index.js index b39a66156..320fac53a 100644 --- a/src/components/shared/index.js +++ b/src/components/shared/index.js @@ -3,6 +3,7 @@ // Exports the shared react components into as a library export * from './ajaxError/ajaxError'; +export * from './componentArray/componentArray'; export * from './contextMenu'; export * from './flyout'; export * from './forms'; diff --git a/src/walkthrough/components/pages/dashboard/dashboard.js b/src/walkthrough/components/pages/dashboard/dashboard.js index 04954d81f..7b333e650 100644 --- a/src/walkthrough/components/pages/dashboard/dashboard.js +++ b/src/walkthrough/components/pages/dashboard/dashboard.js @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import { Grid, Cell } from 'components/pages/dashboard/grid'; import { ExamplePanel } from './panels'; -import { ContextMenu, PageContent } from 'components/shared'; +import { ComponentArray, ContextMenu, PageContent } from 'components/shared'; import './dashboard.css'; @@ -22,35 +22,37 @@ export class Dashboard extends Component { render() { const { t } = this.props; - return [ - - {/** Add context buttons here... as needed for your dashboard. In this example, there are none. */} - , - - - - - - - - - - - - - - - - - - - - - - - - - - ]; + return ( + + + {/** Add context buttons here... as needed for your dashboard. In this example, there are none. */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } } diff --git a/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.js b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.js index 77ec0ce8c..f8876bbec 100644 --- a/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.js +++ b/src/walkthrough/components/pages/pageWithFlyout/pageWithFlyout.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; -import { Btn, ContextMenu, PageContent } from 'components/shared'; +import { Btn, ComponentArray, ContextMenu, PageContent } from 'components/shared'; import { svgs } from 'utilities'; import { ExampleFlyoutContainer } from './flyouts/exampleFlyout'; @@ -26,14 +26,16 @@ export class PageWithFlyout extends Component { const isExampleFlyoutOpen = openFlyoutName === 'example'; - return [ - - {t('walkthrough.pageWithFlyout.open')} - , - - {t('walkthrough.pageWithFlyout.pageBody')} - { isExampleFlyoutOpen && } - - ]; + return ( + + + {t('walkthrough.pageWithFlyout.open')} + + + {t('walkthrough.pageWithFlyout.pageBody')} + {isExampleFlyoutOpen && } + + + ); } } diff --git a/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js b/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js index 622994efb..6c110bfa9 100644 --- a/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js +++ b/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import React, { Component } from 'react'; -import { Btn, PcsGrid } from 'components/shared'; +import { Btn, ComponentArray, PcsGrid } from 'components/shared'; import { exampleColumnDefs, defaultExampleGridProps } from './exampleGridConfig'; import { isFunc, svgs, translateColumnDefs } from 'utilities'; import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig'; @@ -31,10 +31,11 @@ export class ExampleGrid extends Component { // Set up the available context buttons. // If these are subject to user permissions, use the Protected component (src/components/shared/protected). - this.contextBtns = [ - {props.t('walkthrough.pageWithGrid.grid.btn1')}, - {props.t('walkthrough.pageWithGrid.grid.btn2')} - ]; + this.contextBtns = + + {props.t('walkthrough.pageWithGrid.grid.btn1')} + {props.t('walkthrough.pageWithGrid.grid.btn2')} + ; } /** diff --git a/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.js b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.js index 62cf2b4d8..399c7c102 100644 --- a/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.js +++ b/src/walkthrough/components/pages/pageWithGrid/pageWithGrid.js @@ -4,6 +4,7 @@ import React, { Component } from 'react'; import { AjaxError, + ComponentArray, ContextMenu, PageContent, RefreshBar @@ -36,15 +37,17 @@ export class PageWithGrid extends Component { t: this.props.t }; - return [ - - {this.state.contextBtns} - , - - - {!!error && } - {!error && } - - ]; + return ( + + + {this.state.contextBtns} + + + + {!!error && } + {!error && } + + + ); } } From 566e91f939ed0eb8077fbb4ae1b4fde04e757f38 Mon Sep 17 00:00:00 2001 From: Mary Ellen Chaffin Date: Mon, 24 Sep 2018 16:22:45 -0700 Subject: [PATCH 16/25] Followup to remove more keys for ComponentArray usage (#1109) --- src/components/pages/dashboard/dashboard.js | 4 ++-- src/components/pages/deployments/deployments.js | 4 ++-- .../pages/deployments/deploymentsGrid/deploymentsGrid.js | 2 +- .../deployments/flyouts/deploymentNew/deploymentNew.js | 2 +- src/components/pages/devices/devices.js | 4 ++-- .../pages/devices/flyouts/deviceDetails/deviceDetails.js | 6 +++--- .../pages/maintenance/ruleDetails/ruleDetails.js | 8 ++++---- src/components/pages/maintenance/summary/summary.js | 4 ++-- src/components/pages/packages/packages.js | 4 ++-- .../pages/packages/packagesGrid/packagesGrid.js | 2 +- src/components/pages/rules/rules.js | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/pages/dashboard/dashboard.js b/src/components/pages/dashboard/dashboard.js index e0e51a38b..f9b73f248 100644 --- a/src/components/pages/dashboard/dashboard.js +++ b/src/components/pages/dashboard/dashboard.js @@ -359,13 +359,13 @@ export class Dashboard extends Component { return ( - + - + - + - + {this.state.contextBtns} {t('deployments.flyouts.new.contextMenuName')} diff --git a/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js b/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js index c58937a36..2ee516f7a 100644 --- a/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js +++ b/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js @@ -51,7 +51,7 @@ export class DeploymentsGrid extends Component { }; return ( - + ); } } diff --git a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js index 91073e73d..eb63ec2f0 100644 --- a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js +++ b/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js @@ -273,7 +273,7 @@ export class DeploymentNew extends LinkedComponent { {targetedDeviceCount} {t('deployments.flyouts.new.targetText')} - {completedSuccessfully && } + {completedSuccessfully && } } { diff --git a/src/components/pages/devices/devices.js b/src/components/pages/devices/devices.js index 0844c254c..cf67e7ad7 100644 --- a/src/components/pages/devices/devices.js +++ b/src/components/pages/devices/devices.js @@ -80,13 +80,13 @@ export class Devices extends Component { return ( - + - + {this.state.contextBtns} diff --git a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js index 6168f1d8a..a68f843f6 100644 --- a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js +++ b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js @@ -346,11 +346,11 @@ export class DeviceDetails extends Component { { device.connected && - + {t('devices.flyouts.details.diagnostics.lastMessage')} {lastMessageTime ? moment(lastMessageTime).format(DEFAULT_TIME_FORMAT) : '---'} - , - + + {t('devices.flyouts.details.diagnostics.message')} {t('devices.flyouts.details.diagnostics.showMessage')} diff --git a/src/components/pages/maintenance/ruleDetails/ruleDetails.js b/src/components/pages/maintenance/ruleDetails/ruleDetails.js index 4988c287b..f87db9380 100644 --- a/src/components/pages/maintenance/ruleDetails/ruleDetails.js +++ b/src/components/pages/maintenance/ruleDetails/ruleDetails.js @@ -286,13 +286,13 @@ export class RuleDetails extends Component { return ( - + - + { this.state.updatingAlertStatus &&
@@ -405,8 +405,8 @@ export class RuleDetails extends Component { { !isPending && (selectedTab === tabIds.all || selectedTab === tabIds.telemetry) && Object.keys(this.state.telemetry).length > 0 && -

{t('maintenance.alertedDeviceTelemetry')}

-
+

{t('maintenance.alertedDeviceTelemetry')}

+
diff --git a/src/components/pages/maintenance/summary/summary.js b/src/components/pages/maintenance/summary/summary.js index b3c8be05e..e1d9e4445 100644 --- a/src/components/pages/maintenance/summary/summary.js +++ b/src/components/pages/maintenance/summary/summary.js @@ -42,13 +42,13 @@ export const Summary = ({ }) => - + - + - + { /* Add left aligned items as needed */} - + {this.state.contextBtns} {t('packages.new')} diff --git a/src/components/pages/packages/packagesGrid/packagesGrid.js b/src/components/pages/packages/packagesGrid/packagesGrid.js index e53f04364..005cac7ce 100644 --- a/src/components/pages/packages/packagesGrid/packagesGrid.js +++ b/src/components/pages/packages/packagesGrid/packagesGrid.js @@ -32,7 +32,7 @@ export class PackagesGrid extends Component { this.contextBtns = - + {props.t('packages.delete')} ; diff --git a/src/components/pages/rules/rules.js b/src/components/pages/rules/rules.js index 79322ebad..432647639 100644 --- a/src/components/pages/rules/rules.js +++ b/src/components/pages/rules/rules.js @@ -91,13 +91,13 @@ export class Rules extends Component { return ( - + - + {this.state.contextBtns} From e287a27c40282bb22527e4c61a29f6a2781c947e Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 27 Sep 2018 13:05:41 -0700 Subject: [PATCH 17/25] Merge Master with Feature Branch (#1111) * Dev Walkthru: add a new Panel to the Dashboard (#1062) * Dev Walkthru: add a new Panel to the Dashboard * small tweaks, review feedback * fix bad code end marker * Diagnostics bugFix (#1065) * flatMap * Dummy comment to retrigger build * Add Rule Diagnostics (#1064) Add diagnostics logging for rule create/update events. Added the following metrics: Rule_NewClick Rule_EditClick Rule_DeviceGroupClick Rule_CalculationClick Rule_FieldClick Rule_OperatorClick Rule_AddConditionClick Rule_SeverityLevelClick Rule_StatusToggle Rule_ApplyClick Rule_CancelClick Rule_TopXCloseClick Also includes new "sessionid" sections of diagnostics call, which logs the time in ms since Jan 1, 1970 when the page was loaded (amplitude expects session id in this format). This fields will be added by diagnostics to enable logging of session id to amplitude--until those changes go in it will be ignored by the backend. * Delete .travis.yml (#1066) * Delete .travis.yml * Update README.md * Add diagnostics for new device funnel (#1075) * Add device metrics Add metrics for new device flyout * Add device created metric Add metric on device create so we can log device id for physical devices * Fix rule apply event Rule apply click event was only emitted for new rules. Move call so it is emitted if a rule is added or edited. * Address comments and align metric names Address comments. Update metric names to be in same format as rule metrics * fix insertion when entities are null for devices and rules (#1078) * Refactor to make walkthrough code less intrusive (#1069) * refactor to make walkthrough code less intrusive * refactor after meeting with team * update MD files, move httpClient * review feedback * fix nit * update breadcrumbs to use isDef instead of checking undefined * Treat text and number correctly in device jobs (#1082) * treat text and number correctly in device jobs * fix formatting nit * Rule updates need to send ETag (#1084) UI needs to send the ETag when updating rules. * Rule enable/disable needs to update ETag in redux store (#1086) * Small updates for the add page walkthrough (#1089) * Making cloudToDeviceMethod to empty an string (#1090) * rearrange controls on context menus (#1093) * Add links to Time Series Insights from Dashboard and Device Details Page (#1085) Add a Hyperlink shared component Fetch the Time Series Explorer URL from Telemetry Add link to dashboard and device details * Adding curly brackets in json Payload (#1094) * Fix alignment of stats on dashbaord (quick fix) (#1097) * Move Add Condition button below the rule conditions (#1103) * Update section decriptions to be more actionable on Device Details (#1110) --- public/locales/en/translations.json | 16 ++--- .../flyouts/deviceDetails/deviceDetails.js | 28 +++++++- .../pages/maintenance/summary/summary.js | 10 +-- .../rules/flyouts/ruleEditor/ruleEditor.js | 12 ++-- .../components/pages/dashboard/dashboard.js | 64 +++++++++---------- 5 files changed, 75 insertions(+), 55 deletions(-) diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 2375043df..8a6453b39 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -206,12 +206,12 @@ }, "methods": { "title": "Methods", - "description": "All methods available for device", + "description": "To run a method on one or more devices, close this pane, select the checkbox for the device(s), click <1><0>{{jobs}}, and then select <3><0>{{methods}}.", "noneExist": "No methods found for this device." }, "properties": { "title": "Properties", - "description": "Selected properties on device", + "description": "To change a property on one or more devices, close this pane, select the checkbox for the device(s), click <1><0>{{jobs}}, and then select <3><0>{{properties}}.", "keyHeader": "Property", "valueHeader": "Value", "noneExist": "No properties found for this device.", @@ -221,7 +221,7 @@ }, "tags": { "title": "Tags", - "description": "Tags applied to device", + "description": "To add, delete, or change a tag on one or more devices, close this pane, select the checkbox for the device(s), click <1><0>{{jobs}}, and then select <3><0>{{tags}}.", "keyHeader": "Key", "valueHeader": "Value", "noneExist": "No tags found for this device." @@ -265,7 +265,7 @@ "required": "Is required" }, "tags": { - "radioLabel": "Tag", + "radioLabel": "Tags", "title": "Tags on selected devices", "description": "Tags in common on selected devices", "keyHeader": "Key", @@ -277,8 +277,8 @@ "add": "Add tag" }, "methods": { - "radioLabel": "Run method", - "title": "Run method", + "radioLabel": "Methods", + "title": "Run method on selected devices", "description": "All available methods for device or group of devices", "methodName": "Method name", "methodNameHint": "Select method", @@ -288,8 +288,8 @@ "firmwareUriHint": "Enter firmware URI value" }, "properties": { - "radioLabel": "Reconfigure", - "title": "Reconfigure", + "radioLabel": "Properties", + "title": "Properties on selected devices", "description": "Available properties and values to change", "keyHeader": "Property", "valueHeader": "Value", diff --git a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js index a68f843f6..1b7bd344e 100644 --- a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js +++ b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import React, { Component } from 'react'; +import { Trans } from 'react-i18next'; import { Subject } from 'rxjs'; import moment from 'moment'; import { DEFAULT_TIME_FORMAT } from 'components/shared/pcsGrid/pcsGridConfig'; @@ -230,7 +231,14 @@ export class DeviceDetails extends Component { {t('devices.flyouts.details.tags.title')} - {t('devices.flyouts.details.tags.description')} + + + To edit, close this panel, click on + {{ jobs: t('devices.flyouts.jobs.title') }} + then select + {{ tags: t('devices.flyouts.jobs.tags.radioLabel') }}. + + { (tags.length === 0) && t('devices.flyouts.details.tags.noneExist') @@ -262,7 +270,14 @@ export class DeviceDetails extends Component { {t('devices.flyouts.details.methods.title')} - {t('devices.flyouts.details.methods.description')} + + + To edit, close this panel, click on + {{ jobs: t('devices.flyouts.jobs.title') }} + then select + {{ methods: t('devices.flyouts.jobs.methods.radioLabel') }}. + + { (device.methods.length === 0) ? t('devices.flyouts.details.methods.noneExist') @@ -283,7 +298,14 @@ export class DeviceDetails extends Component { {t('devices.flyouts.details.properties.title')} - {t('devices.flyouts.details.properties.description')} + + + To edit, close this panel, click on + {{ jobs: t('devices.flyouts.jobs.title') }} + then select + {{ properties: t('devices.flyouts.jobs.properties.radioLabel') }}. + + { (properties.length === 0) && t('devices.flyouts.details.properties.noneExist') diff --git a/src/components/pages/maintenance/summary/summary.js b/src/components/pages/maintenance/summary/summary.js index e1d9e4445..4119cfb6d 100644 --- a/src/components/pages/maintenance/summary/summary.js +++ b/src/components/pages/maintenance/summary/summary.js @@ -56,11 +56,11 @@ export const Summary = ({ - + diff --git a/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js b/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js index 404ab0628..0e703aa32 100644 --- a/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js +++ b/src/components/pages/rules/flyouts/ruleEditor/ruleEditor.js @@ -370,12 +370,6 @@ export class RuleEditor extends LinkedComponent { { !fieldQueryPending &&
- - {t('rules.flyouts.ruleEditor.conditions')} - - {t('rules.flyouts.ruleEditor.addCondition')} - - { conditionLinks.map((condition, idx) => ( @@ -421,6 +415,12 @@ export class RuleEditor extends LinkedComponent { )) } + + {t('rules.flyouts.ruleEditor.conditions')} + + {t('rules.flyouts.ruleEditor.addCondition')} + + diff --git a/src/walkthrough/components/pages/dashboard/dashboard.js b/src/walkthrough/components/pages/dashboard/dashboard.js index 7b333e650..04954d81f 100644 --- a/src/walkthrough/components/pages/dashboard/dashboard.js +++ b/src/walkthrough/components/pages/dashboard/dashboard.js @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import { Grid, Cell } from 'components/pages/dashboard/grid'; import { ExamplePanel } from './panels'; -import { ComponentArray, ContextMenu, PageContent } from 'components/shared'; +import { ContextMenu, PageContent } from 'components/shared'; import './dashboard.css'; @@ -22,37 +22,35 @@ export class Dashboard extends Component { render() { const { t } = this.props; - return ( - - - {/** Add context buttons here... as needed for your dashboard. In this example, there are none. */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + return [ + + {/** Add context buttons here... as needed for your dashboard. In this example, there are none. */} + , + + + + + + + + + + + + + + + + + + + + + + + + + + ]; } } From 4edda60821552d0f0e1b9b4357456fee21f555b3 Mon Sep 17 00:00:00 2001 From: Mary Ellen Chaffin Date: Thu, 27 Sep 2018 15:17:20 -0700 Subject: [PATCH 18/25] Fix device insert reducer function so the id in the items list is a string (not an array) (#1114) --- src/store/reducers/devicesReducer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store/reducers/devicesReducer.js b/src/store/reducers/devicesReducer.js index 6d0a3e4d8..0be6619e1 100644 --- a/src/store/reducers/devicesReducer.js +++ b/src/store/reducers/devicesReducer.js @@ -97,12 +97,12 @@ const insertDevicesReducer = (state, { payload }) => { if (state.entities) { return update(state, { entities: { $merge: devices }, - items: { $splice: [[0, 0, result]] } + items: { $splice: [[0, 0, ...result]] } }); } return update(state, { entities: { $set: devices }, - items: { $set: [result] } + items: { $set: result } }); }; From 6202d53181552023a4678c9891f61958d7fd3387 Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 28 Sep 2018 18:58:59 -0700 Subject: [PATCH 19/25] Adding Deployment Details Page (#1112) * Squash DeploymentDetails commits * initial review fixes * review comments fix part 2 * fix for Stephen's review comments * adding test page and minor fixes * ByQuery * fix deployments tests --- public/locales/en/translations.json | 30 ++- src/assets/icons/failed.svg | 11 ++ src/components/app.js | 12 +- .../deploymentDetails.container.js | 33 ++++ .../deploymentDetails/deploymentDetails.js | 172 ++++++++++++++++++ .../deploymentDetails/deploymentDetails.scss | 49 +++++ .../deploymentDetails.test.js | 23 +++ .../deploymentDetailsGrid.js | 36 ++++ .../deploymentDetailsGridConfig.js | 52 ++++++ .../deployments/deploymentDetails/index.js | 4 + .../deployments.container.js | 0 .../{ => deploymentsHome}/deployments.js | 5 +- .../{ => deploymentsHome}/deployments.scss | 3 +- .../{ => deploymentsHome}/deployments.test.js | 1 + .../deploymentsGrid/deploymentsGrid.js | 0 .../deploymentsGrid/deploymentsGridConfig.js | 0 .../deploymentsGrid/index.js | 0 .../deploymentNew/deploymentNew.container.js | 0 .../flyouts/deploymentNew/deploymentNew.js | 30 +-- .../flyouts/deploymentNew/deploymentNew.scss | 0 .../flyouts/deploymentNew/index.js | 0 .../{ => deploymentsHome}/flyouts/index.js | 1 - .../deployments/deploymentsHome/index.js | 4 + .../pages/deployments/deploymentsRouter.js | 14 ++ .../deploymentDelete.container.js | 23 --- .../deploymentDelete/deploymentDelete.js | 41 ----- .../deploymentDelete/deploymentDelete.scss | 9 - .../flyouts/deploymentDelete/index.js | 4 - src/components/pages/index.js | 2 +- .../pages/maintenance/summary/summary.js | 2 +- .../pages/maintenance/summary/summary.scss | 2 + .../pageStats/statProperty/statProperty.scss | 3 + .../pageStats/statSection/statSection.scss | 2 +- src/services/iotHubManagerService.js | 27 ++- src/services/models/iotHubManagerModels.js | 41 +++-- src/store/reducers/deploymentsReducer.js | 114 ++++++++++-- src/styles/_themes.scss | 1 + src/styles/_variables.scss | 1 + src/utilities/svgs.js | 2 + 39 files changed, 626 insertions(+), 128 deletions(-) create mode 100644 src/assets/icons/failed.svg create mode 100644 src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js create mode 100644 src/components/pages/deployments/deploymentDetails/deploymentDetails.js create mode 100644 src/components/pages/deployments/deploymentDetails/deploymentDetails.scss create mode 100644 src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js create mode 100644 src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js create mode 100644 src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js create mode 100644 src/components/pages/deployments/deploymentDetails/index.js rename src/components/pages/deployments/{ => deploymentsHome}/deployments.container.js (100%) rename src/components/pages/deployments/{ => deploymentsHome}/deployments.js (96%) rename src/components/pages/deployments/{ => deploymentsHome}/deployments.scss (78%) rename src/components/pages/deployments/{ => deploymentsHome}/deployments.test.js (92%) rename src/components/pages/deployments/{ => deploymentsHome}/deploymentsGrid/deploymentsGrid.js (100%) rename src/components/pages/deployments/{ => deploymentsHome}/deploymentsGrid/deploymentsGridConfig.js (100%) rename src/components/pages/deployments/{ => deploymentsHome}/deploymentsGrid/index.js (100%) rename src/components/pages/deployments/{ => deploymentsHome}/flyouts/deploymentNew/deploymentNew.container.js (100%) rename src/components/pages/deployments/{ => deploymentsHome}/flyouts/deploymentNew/deploymentNew.js (100%) rename src/components/pages/deployments/{ => deploymentsHome}/flyouts/deploymentNew/deploymentNew.scss (100%) rename src/components/pages/deployments/{ => deploymentsHome}/flyouts/deploymentNew/index.js (100%) rename src/components/pages/deployments/{ => deploymentsHome}/flyouts/index.js (69%) create mode 100644 src/components/pages/deployments/deploymentsHome/index.js create mode 100644 src/components/pages/deployments/deploymentsRouter.js delete mode 100644 src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.container.js delete mode 100644 src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.js delete mode 100644 src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.scss delete mode 100644 src/components/pages/deployments/flyouts/deploymentDelete/index.js diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 8a6453b39..a44b7d21d 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -565,13 +565,41 @@ "priorityPlaceHolder": "Enter priority", "namePlaceHolder": "Enter name", "targetText": "targeted devices", - "infoText": "* Edge Manifest packages that you deploy will be applied to every edge device group in the device group and to any edge group that you add to the device group in the future. The deployment will run continously, verifying that current and future devices are configured correctly (and automatically updating that aren't). Package wont be applied to non-edge devices.", + "infoText": "* This deployment runs continuously. Every edge device (and any you add in the future) in the selected device group will receive this package.", "successText": "View your deployment status detail for {{deplymentName}}", "creating": "Creating deployment", "validation": { "required": "Is required", "nan": "Must be a number" } + }, + "delete": { + "contextMenuName": "Delete", + "title": "Delete Deployment?", + "delete": "Delete", + "cancel": "Cancel", + "info": "Deleting {deploymentName} will stop this deployment from being applied to these devices. It may result in a lower priority deployment being applied" + } + }, + "details": { + "deploymentName": "Deployment name", + "deviceGroup": "Device group", + "start": "Start", + "packageType": "Package type", + "package": "Package", + "priority": "Priority", + "devices": "Devices", + "failed": "Failed", + "succeeded": "Succeeded", + "pending": "Pending", + "devicesAffected": "Devices Affected", + "grid": { + "name": "Name", + "deploymentStatus": "Deployment Status", + "firmware": "Firmware", + "lastMessage": "Last Message", + "start": "Start", + "end": "End" } }, "typeOptions": { diff --git a/src/assets/icons/failed.svg b/src/assets/icons/failed.svg new file mode 100644 index 000000000..5279c0133 --- /dev/null +++ b/src/assets/icons/failed.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/components/app.js b/src/components/app.js index 3dab1e33e..543477991 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -11,7 +11,7 @@ import { RulesContainer, MaintenanceContainer, PackagesContainer, - DeploymentsContainer + DeploymentsRouter } from './pages'; class App extends Component { @@ -61,10 +61,10 @@ class App extends Component { }, { to: '/deployments', - exact: true, + exact: false, svg: svgs.tabs.deployments, labelId: 'tabs.deployments', - component: DeploymentsContainer + component: DeploymentsRouter }, { to: '/maintenance', @@ -101,6 +101,12 @@ class App extends Component { { to: '/deployments', labelId: 'tabs.deployments' } ] }, + { + path: '/deployments/:id', crumbs: [ + { to: '/deployments', labelId: 'tabs.deployments' }, + { to: '/deployments/:id', matchParam: 'id' }, + ] + }, { path: '/maintenance', crumbs: [ { to: '/maintenance', labelId: 'tabs.maintenance' } diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js new file mode 100644 index 000000000..d0ebcdd61 --- /dev/null +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { translate } from 'react-i18next'; +import { connect } from 'react-redux'; +import { DeploymentDetails } from './deploymentDetails'; +import { + getCurrentDeploymentDetailsError, + getCurrentDeploymentDetailsPendingStatus, + getCurrentDeploymentLastUpdated, + getCurrentDeploymentDetails, + getDeployedDevicesPendingStatus, + getDeployedDevicesError, + getDeployedDevices, + epics as deploymentsEpics +} from 'store/reducers/deploymentsReducer'; + +// Pass the global info needed +const mapStateToProps = state => ({ + isPending: getCurrentDeploymentDetailsPendingStatus(state), + error: getCurrentDeploymentDetailsError(state), + currentDeployment: getCurrentDeploymentDetails(state), + isDeployedDevicesPending: getDeployedDevicesPendingStatus(state), + deployedDevicesError: getDeployedDevicesError(state), + deployedDevices: getDeployedDevices(state), + lastUpdated: getCurrentDeploymentLastUpdated(state) +}); + +// Wrap the dispatch methods +const mapDispatchToProps = dispatch => ({ + fetchDeployment: (id) => dispatch(deploymentsEpics.actions.fetchDeployment(id)) +}); + +export const DeploymentDetailsContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeploymentDetails)); diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js new file mode 100644 index 000000000..a792f606a --- /dev/null +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; +import { permissions } from 'services/models'; +import { + AjaxError, + Btn, + ComponentArray, + ContextMenu, + ContextMenuAlign, + Indicator, + PageContent, + Protected, + RefreshBar, + StatSection, + StatGroup, + StatProperty +} from 'components/shared'; +import { TimeRenderer } from 'components/shared/cellRenderers'; +import { svgs } from 'utilities'; +import { DeploymentDetailsGrid } from './deploymentDetailsGrid/deploymentDetailsGrid'; + +import "./deploymentDetails.css"; + +export class DeploymentDetails extends Component { + constructor(props) { + super(props); + props.fetchDeployment(props.match.params.id); + } + + render() { + const { + t, + currentDeployment, + isPending, + error, + deployedDevices, + isDeployedDevicesPending, + deployedDevicesError, + fetchDeployment, + lastUpdated + } = this.props; + const { + appliedCount, + succeededCount, + failedCount, + name, + priority, + deviceGroupId, + createdDateTimeUtc, + type, + packageId + } = currentDeployment; + const pendingCalc = appliedCount - succeededCount - failedCount; + const pendingCount = pendingCalc ? pendingCalc : '0'; + + return ( + + + + + {t('deployments.flyouts.delete.contextMenuName')} + + + + + + {!!error && } + {isPending && } + + { + !isPending && +
+
+ {t('deployments.details.deploymentName')} +
+
+ {name} +
+ + + + +
+ {t('deployments.details.priority')} +
+
+ {priority} +
+
+
+ + + + + + + + + +
+ {t('deployments.details.deviceGroup')} +
+
+ {deviceGroupId} +
+
+ +
+ {t('deployments.details.start')} +
+
+ {TimeRenderer({ value: createdDateTimeUtc })} +
+
+
+ + +
+ {t('deployments.details.packageType')} +
+
+ {type} +
+
+ +
+ {t('deployments.details.package')} +
+
+ {packageId} +
+
+
+
+
+ } + +

+ {t('deployments.details.devicesAffected')} +

+ + {isDeployedDevicesPending && } + + { + deployedDevicesError && + + } + + {!isDeployedDevicesPending && } +
+
+ ); + } +} diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.scss b/src/components/pages/deployments/deploymentDetails/deploymentDetails.scss new file mode 100644 index 000000000..662102296 --- /dev/null +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.scss @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/variables'; +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.deployments-details-container { + display: flex; + flex-flow: column nowrap; + flex-grow: 1; + + .deployment-details-summary-container { + .deployment-name{ + font-weight: 700; + @include rem-font-size(34px); + @include rem-fallback(padding, 10px, 24px, 0px, 0px); + } + + .deployment-details-summary-labels { + text-transform: uppercase; + @include rem-font-size(12px); + @include rem-fallback(padding-bottom, 10px); + } + + .stat-failed { @include rem-font-size(16px); } + + .summary-container { @include rem-fallback(padding-top, 30px); } + + .summary-container-columns { @include rem-fallback(min-width, 200px); } + + .summary-container-second-row { @include rem-fallback(padding-top, 20px); } + } + + .deployment-details-devices-affected { + @include rem-fallback(border-top, 1px, solid); + @include rem-fallback(padding, 35px, 0px, 0px, 0px); + @include rem-fallback(margin-top, 50px); + } + + @include themify($themes) { + .deployment-details-summary-values { color: themed('colorContentText'); } + + .deployment-details-summary-labels, .deployment-name { color: themed('colorHeaderText'); } + + .stat-failed { fill: themed('colorFailed'); } + + .deployment-details-devices-affected { border-color: themed('colorHeaderBorderColor'); } + } +} diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js new file mode 100644 index 000000000..927504cf0 --- /dev/null +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; +import { shallow } from 'enzyme'; +import 'polyfills'; + +import { DeploymentDetails } from './deploymentDetails'; + +describe('Deployment details Component', () => { + it('Renders without crashing', () => { + + const fakeProps = { + t: () => { }, + match: { params: { id: 'testId' } }, + fetchDeployment: () => { }, + currentDeployment: {} + }; + + const wrapper = shallow( + + ); + }); +}); diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js new file mode 100644 index 000000000..7af7034fe --- /dev/null +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React, { Component } from 'react'; +import { deploymentDetailsColumnDefs, defaultDeploymentDetailsGridProps } from './deploymentDetailsGridConfig'; +import { translateColumnDefs } from 'utilities'; +import { PcsGrid } from 'components/shared'; + +export class DeploymentDetailsGrid extends Component { + constructor(props) { + super(props); + + this.columnDefs = [ + deploymentDetailsColumnDefs.name, + deploymentDetailsColumnDefs.deploymentStatus, + deploymentDetailsColumnDefs.firmware, + deploymentDetailsColumnDefs.lastMessage, + deploymentDetailsColumnDefs.start, + deploymentDetailsColumnDefs.end + ]; + } + + onGridReady = gridReadyEvent => this.deployedDevicesGridApi = gridReadyEvent.api; + + render() { + const gridProps = { + /* Grid Properties */ + ...defaultDeploymentDetailsGridProps, + t: this.props.t, + rowData: this.props.deployedDevices, + getRowNodeId: ({ id }) => id, + columnDefs: translateColumnDefs(this.props.t, this.columnDefs) + }; + + return (); + } +} diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js new file mode 100644 index 000000000..8caa21bfc --- /dev/null +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +import Config from 'app.config'; +import { TimeRenderer } from 'components/shared/cellRenderers'; +import { gridValueFormatters } from 'components/shared/pcsGrid/pcsGridConfig'; + +const { checkForEmpty } = gridValueFormatters; + +export const deploymentDetailsColumnDefs = { + name: { + headerName: 'deployments.details.grid.name', + field: 'id', + sort: 'asc', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + deploymentStatus: { + headerName: 'deployments.details.grid.deploymentStatus', + field: 'deploymentStatus', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + firmware: { + headerName: 'deployments.details.grid.firmware', + field: 'firmware', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + lastMessage: { + headerName: 'deployments.details.grid.lastMessage', + field: 'lastMessage', + valueFormatter: ({ value }) => checkForEmpty(value) + }, + start: { + headerName: 'deployments.details.grid.start', + field: 'start', + cellRendererFramework: TimeRenderer + }, + end: { + headerName: 'deployments.details.grid.end', + field: 'end', + cellRendererFramework: TimeRenderer + } +}; + +export const defaultDeploymentDetailsGridProps = { + enableColResize: true, + pagination: true, + paginationPageSize: Config.paginationPageSize, + sizeColumnsToFit: true, + deltaRowDataMode: true, + enableSorting: true, + unSortIcon: true, + domLayout: 'autoHeight' +}; diff --git a/src/components/pages/deployments/deploymentDetails/index.js b/src/components/pages/deployments/deploymentDetails/index.js new file mode 100644 index 000000000..beeb79315 --- /dev/null +++ b/src/components/pages/deployments/deploymentDetails/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './deploymentDetails'; +export * from './deploymentDetails.container'; diff --git a/src/components/pages/deployments/deployments.container.js b/src/components/pages/deployments/deploymentsHome/deployments.container.js similarity index 100% rename from src/components/pages/deployments/deployments.container.js rename to src/components/pages/deployments/deploymentsHome/deployments.container.js diff --git a/src/components/pages/deployments/deployments.js b/src/components/pages/deployments/deploymentsHome/deployments.js similarity index 96% rename from src/components/pages/deployments/deployments.js rename to src/components/pages/deployments/deploymentsHome/deployments.js index a34802daa..cb47228e8 100644 --- a/src/components/pages/deployments/deployments.js +++ b/src/components/pages/deployments/deploymentsHome/deployments.js @@ -58,13 +58,14 @@ export class Deployments extends Component { onGridReady = gridReadyEvent => this.deploymentGridApi = gridReadyEvent.api; render() { - const { t, deployments, error, isPending, fetchDeployments, lastUpdated } = this.props; + const { t, deployments, error, isPending, fetchDeployments, lastUpdated, history } = this.props; const gridProps = { onGridReady: this.onGridReady, rowData: isPending ? undefined : deployments || [], refresh: fetchDeployments, onContextMenuChange: this.onContextMenuChange, - t: t + t: t, + onRowClicked: ({ data: { id } }) => history.push(`/deployments/${id}`) }; return ( diff --git a/src/components/pages/deployments/deployments.scss b/src/components/pages/deployments/deploymentsHome/deployments.scss similarity index 78% rename from src/components/pages/deployments/deployments.scss rename to src/components/pages/deployments/deploymentsHome/deployments.scss index 7f5d37d16..d388971c2 100644 --- a/src/components/pages/deployments/deployments.scss +++ b/src/components/pages/deployments/deploymentsHome/deployments.scss @@ -4,7 +4,8 @@ @import 'src/styles/mixins'; @import 'src/styles/themes'; -.deployments-page-container { +.deployments-page-container, +.deployments-details-container { display: flex; flex-flow: column nowrap; padding: $baseContentPadding; diff --git a/src/components/pages/deployments/deployments.test.js b/src/components/pages/deployments/deploymentsHome/deployments.test.js similarity index 92% rename from src/components/pages/deployments/deployments.test.js rename to src/components/pages/deployments/deploymentsHome/deployments.test.js index 15cb79866..268ac3744 100644 --- a/src/components/pages/deployments/deployments.test.js +++ b/src/components/pages/deployments/deploymentsHome/deployments.test.js @@ -11,6 +11,7 @@ describe('Deployments Component', () => { const fakeProps = { t: () => {}, + fetchDeployments: () => {} }; const wrapper = shallow( diff --git a/src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js b/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGrid.js similarity index 100% rename from src/components/pages/deployments/deploymentsGrid/deploymentsGrid.js rename to src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGrid.js diff --git a/src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js b/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js similarity index 100% rename from src/components/pages/deployments/deploymentsGrid/deploymentsGridConfig.js rename to src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js diff --git a/src/components/pages/deployments/deploymentsGrid/index.js b/src/components/pages/deployments/deploymentsHome/deploymentsGrid/index.js similarity index 100% rename from src/components/pages/deployments/deploymentsGrid/index.js rename to src/components/pages/deployments/deploymentsHome/deploymentsGrid/index.js diff --git a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.container.js b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js similarity index 100% rename from src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.container.js rename to src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js diff --git a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js similarity index 100% rename from src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js rename to src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js index eb63ec2f0..a725daa09 100644 --- a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.js +++ b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js @@ -191,21 +191,6 @@ export class DeploymentNew extends LinkedComponent { completedSuccessfully && {name} }
- - {t('deployments.flyouts.new.priority')} - { - !completedSuccessfully && - - } - { - completedSuccessfully && {priority} - } - {t('deployments.flyouts.new.type')} { @@ -266,6 +251,21 @@ export class DeploymentNew extends LinkedComponent { completedSuccessfully && {deviceGroupName} } + + {t('deployments.flyouts.new.priority')} + { + !completedSuccessfully && + + } + { + completedSuccessfully && {priority} + } + {/** Displays targeted devices count once device goup is selected. */ diff --git a/src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.scss b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.scss similarity index 100% rename from src/components/pages/deployments/flyouts/deploymentNew/deploymentNew.scss rename to src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.scss diff --git a/src/components/pages/deployments/flyouts/deploymentNew/index.js b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/index.js similarity index 100% rename from src/components/pages/deployments/flyouts/deploymentNew/index.js rename to src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/index.js diff --git a/src/components/pages/deployments/flyouts/index.js b/src/components/pages/deployments/deploymentsHome/flyouts/index.js similarity index 69% rename from src/components/pages/deployments/flyouts/index.js rename to src/components/pages/deployments/deploymentsHome/flyouts/index.js index dfd6d6f60..91b0e18be 100644 --- a/src/components/pages/deployments/flyouts/index.js +++ b/src/components/pages/deployments/deploymentsHome/flyouts/index.js @@ -1,4 +1,3 @@ // Copyright (c) Microsoft. All rights reserved. export * from './deploymentNew'; -export * from './deploymentDelete'; diff --git a/src/components/pages/deployments/deploymentsHome/index.js b/src/components/pages/deployments/deploymentsHome/index.js new file mode 100644 index 000000000..475baf036 --- /dev/null +++ b/src/components/pages/deployments/deploymentsHome/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './deployments'; +export * from './deployments.container'; diff --git a/src/components/pages/deployments/deploymentsRouter.js b/src/components/pages/deployments/deploymentsRouter.js new file mode 100644 index 000000000..af6da2c9f --- /dev/null +++ b/src/components/pages/deployments/deploymentsRouter.js @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; +import { Route, Redirect, Switch } from 'react-router-dom'; +import { DeploymentDetailsContainer } from './deploymentDetails'; +import { DeploymentsContainer } from './deploymentsHome'; + +export const DeploymentsRouter = () => ( + + } /> + } /> + + +); diff --git a/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.container.js b/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.container.js deleted file mode 100644 index f4e8f719e..000000000 --- a/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.container.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -import { connect } from 'react-redux'; -import { translate } from 'react-i18next'; -import { DeploymentDelete } from './deploymentDelete'; -import { - getDeleteDeploymentError, - getDeleteDeploymentPendingStatus, - epics as deploymentsEpics -} from 'store/reducers/deploymentsReducer'; - -// Pass the global info needed -const mapStateToProps = state => ({ - isPending: getDeleteDeploymentPendingStatus(state), - error: getDeleteDeploymentError(state) -}); - -// Wrap the dispatch methods -const mapDispatchToProps = dispatch => ({ - deleteDeployment: deploymentId => dispatch(deploymentsEpics.actions.deleteDeployment(deploymentId)) -}); - -export const DeploymentDeleteContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeploymentDelete)); diff --git a/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.js b/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.js deleted file mode 100644 index 4e823f9e3..000000000 --- a/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.js +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -import React from 'react'; - -import { LinkedComponent } from 'utilities'; -import { Modal } from 'components/shared'; - -import './deploymentDelete.css'; - -export class DeploymentDelete extends LinkedComponent { - - constructor(props) { - super(props); - - this.state = { - changesApplied: false - }; - } - - componentWillReceiveProps({ error, isPending, onClose }) { - if (this.state.changesApplied && !error && !isPending) { - onClose(); - } - } - - apply = () => { - const { deletePackage, package: { id } } = this.props; - deletePackage(id); - this.setState({ changesApplied: true }); - } - - render() { - const { onClose } = this.props; - - return ( - - TODO - - ); - } -} diff --git a/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.scss b/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.scss deleted file mode 100644 index 887bad0f9..000000000 --- a/src/components/pages/deployments/flyouts/deploymentDelete/deploymentDelete.scss +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -@import 'src/styles/variables'; -@import 'src/styles/mixins'; -@import 'src/styles/themes'; - -.delete-deployment-container { - // TODO - add this in modal -> @include rem-fallback(font-size, 14px); -} diff --git a/src/components/pages/deployments/flyouts/deploymentDelete/index.js b/src/components/pages/deployments/flyouts/deploymentDelete/index.js deleted file mode 100644 index 5940b42f3..000000000 --- a/src/components/pages/deployments/flyouts/deploymentDelete/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -export * from './deploymentDelete'; -export * from './deploymentDelete.container'; diff --git a/src/components/pages/index.js b/src/components/pages/index.js index 693c84340..6562fe197 100644 --- a/src/components/pages/index.js +++ b/src/components/pages/index.js @@ -6,4 +6,4 @@ export * from './devices/devices.container'; export * from './rules/rules.container'; export * from './maintenance/maintenance.container'; export * from './packages/packages.container'; -export * from './deployments/deployments.container'; +export * from './deployments/deploymentsRouter'; diff --git a/src/components/pages/maintenance/summary/summary.js b/src/components/pages/maintenance/summary/summary.js index 4119cfb6d..d72499408 100644 --- a/src/components/pages/maintenance/summary/summary.js +++ b/src/components/pages/maintenance/summary/summary.js @@ -62,7 +62,7 @@ export const Summary = ({ isPending={alertProps.isPending || jobProps.isPending} t={props.t} /> - + id); } + + /** Returns deployments */ + static getDeploymentDetails(query) { + return HttpClient.post(`${ENDPOINT}modules`, query) + .map(toDeploymentsModel); + } } diff --git a/src/services/models/iotHubManagerModels.js b/src/services/models/iotHubManagerModels.js index 3e503bcc5..1ca5d3f8e 100644 --- a/src/services/models/iotHubManagerModels.js +++ b/src/services/models/iotHubManagerModels.js @@ -178,19 +178,24 @@ export const toDevicePropertiesModel = (iotResponse, dsResponse) => { return [...propertySet]; }; -export const toDeploymentModel = (deployment = {}) => camelCaseReshape(deployment, { - 'id': 'id', - 'name': 'name', - 'deviceGroupId': 'deviceGroupId', - 'packageId': 'packageId', - 'priority': 'priority', - 'type': 'type', - 'createdDateTimeUtc': 'createdDateTimeUtc', - 'metrics.appliedCount': 'appliedCount', - 'metrics.failedCount': 'failedCount', - 'metrics.succeededCount': 'succeededCount', - 'metrics.targetedCount': 'targetedCount' -}); +export const toDeploymentModel = (deployment = {}) => { + const modelData = camelCaseReshape(deployment, { + 'id': 'id', + 'name': 'name', + 'deviceGroupId': 'deviceGroupId', + 'packageId': 'packageId', + 'priority': 'priority', + 'type': 'type', + 'createdDateTimeUtc': 'createdDateTimeUtc', + 'metrics.appliedCount': 'appliedCount', + 'metrics.failedCount': 'failedCount', + 'metrics.succeededCount': 'succeededCount', + 'metrics.targetedCount': 'targetedCount' + }); + return update(modelData, { + deviceStatuses: { $set: dot.pick('Metrics.DeviceStatuses', deployment) } + }); +} export const toDeploymentsModel = (response = {}) => getItems(response) .map(toDeploymentModel); @@ -202,3 +207,13 @@ export const toDeploymentRequestModel = (deploymentModel = {}) => ({ Priority: deploymentModel.priority, Type: deploymentModel.type }); + +export const toEdgeAgentModel = (edgeAgent = {}) => camelCaseReshape(edgeAgent, { + 'deviceId': 'id', + 'reported.lastDesiredStatus.code': 'lastMessage', + 'reported.systemModules.edgeAgent.lastStartTimeUtc': 'start', + 'reported.systemModules.edgeAgent.lastExitTimeUtc': 'end' +}); + +export const toEdgeAgentsModel = (response = []) => getItems(response) + .map(toEdgeAgentModel); diff --git a/src/store/reducers/deploymentsReducer.js b/src/store/reducers/deploymentsReducer.js index 744ae4a21..ff71f9a9a 100644 --- a/src/store/reducers/deploymentsReducer.js +++ b/src/store/reducers/deploymentsReducer.js @@ -5,6 +5,7 @@ import { Observable } from 'rxjs'; import moment from 'moment'; import { schema, normalize } from 'normalizr'; import update from 'immutability-helper'; +import dot from 'dot-object'; import { createSelector } from 'reselect'; import { IoTHubManagerService } from 'services'; import { getActiveDeviceGroupId, getActiveDeviceGroupConditions } from './appReducer'; @@ -25,8 +26,16 @@ import { const handleError = fromAction => error => Observable.of(redux.actions.registerError(fromAction.type, { error, fromAction })); +const getDeployedDeviceIds = (payload) => { + return Object.keys(dot.pick('deviceStatuses', payload)) + .map(id => `'${id}'`) + .join(); +} +const createEdgeAgentQuery = (ids) => `"deviceId IN [${ids}] AND moduleId = '$edgeAgent'"`; +const createDevicesQuery = (ids) => `"deviceId IN [${ids}]"`; + export const epics = createEpicScenario({ - /** Loads Deployments */ + /** Loads all Deployments */ fetchDeployments: { type: 'DEPLOYMENTS_FETCH', epic: fromAction => @@ -34,6 +43,27 @@ export const epics = createEpicScenario({ .map(toActionCreator(redux.actions.updateDeployments, fromAction)) .catch(handleError(fromAction)) }, + /** Loads a single Deployment */ + fetchDeployment: { + type: 'DEPLOYMENT_DETAILS_FETCH', + epic: fromAction => IoTHubManagerService.getDeployment(fromAction.payload) + .flatMap(response => [ + toActionCreator(redux.actions.updateDeployment, fromAction)(response), + epics.actions.fetchDeployedDevices(response) + ]) + .catch(handleError(fromAction)) + }, + /** Loads the queried edgeAgents and devices */ + fetchDeployedDevices: { + type: 'DEPLOYED_DEVICES_FETCH', + epic: fromAction => Observable + .forkJoin( + IoTHubManagerService.getEdgeAgentsByQuery(createEdgeAgentQuery(getDeployedDeviceIds(fromAction.payload))), + IoTHubManagerService.getDevicesByQuery(createDevicesQuery(getDeployedDeviceIds(fromAction.payload))), + ) + .map(toActionCreator(redux.actions.updateDeployedDevices, fromAction)) + .catch(handleError(fromAction)) + }, /** Create a new deployment */ createDeployment: { type: 'DEPLOYMENTS_CREATE', @@ -56,22 +86,24 @@ export const epics = createEpicScenario({ // ========================= Schemas - START const deploymentSchema = new schema.Entity('deployments'); const deploymentListSchema = new schema.Array(deploymentSchema); +const deployedDevicesSchema = new schema.Entity('deployedDevices'); +const deployedDevicesListSchema = new schema.Array(deployedDevicesSchema); // ========================= Schemas - END // ========================= Reducers - START const initialState = { ...errorPendingInitialState, entities: {} }; const insertDeploymentReducer = (state, { payload, fromAction }) => { - const { entities: { deployments }, result } = normalize({...payload, isNew: true}, deploymentSchema); + const { entities: { deployments }, result } = normalize({ ...payload, isNew: true }, deploymentSchema); if (state.entities) { return update(state, { - entities: { $merge: deployments }, + entities: { deployments: { $merge: deployments } }, items: { $splice: [[0, 0, result]] }, ...setPending(fromAction.type, false) }); } return update(state, { - entities: { $set: deployments }, + entities: { deployments: { $set: deployments } }, items: { $set: [result] }, ...setPending(fromAction.type, false) }); @@ -80,7 +112,7 @@ const insertDeploymentReducer = (state, { payload, fromAction }) => { const deleteDeploymentReducer = (state, { payload, fromAction }) => { const idx = state.items.indexOf(payload); return update(state, { - entities: { $unset: [payload] }, + entities: { deployments: { $unset: [payload] } }, items: { $splice: [[idx, 1]] }, ...setPending(fromAction.type, false) }); @@ -89,24 +121,60 @@ const deleteDeploymentReducer = (state, { payload, fromAction }) => { const updateDeploymentsReducer = (state, { payload, fromAction }) => { const { entities: { deployments }, result } = normalize(payload, deploymentListSchema); return update(state, { - entities: { $set: deployments }, + entities: { deployments: { $set: deployments } }, items: { $set: result }, lastUpdated: { $set: moment() }, ...setPending(fromAction.type, false) }); }; +const updateDeploymentReducer = (state, { payload, fromAction }) => { + const { deviceStatuses } = payload || {}; + return update(state, { + currentDeployment: { $set: payload }, + deviceStatuses: { $set: deviceStatuses }, + currentDeploymentLastUpdated: { $set: moment() }, + ...setPending(fromAction.type, false) + }); +}; + +const updateDeployedDevicesReducer = (state, { payload: [modules, devices], fromAction }) => { + const normalizedDevices = normalize(devices, deployedDevicesListSchema).entities.deployedDevices; + const normalizedModules = normalize(modules, deployedDevicesListSchema).entities.deployedDevices; + const deployedDevices = Object.keys(normalizedDevices) + .reduce( + (acc, deviceId) => ({ + ...acc, + [deviceId]: { + ...(acc[deviceId] || {}), + firmware: normalizedDevices[deviceId].firmware + } + }), + normalizedModules + ); + return update(state, { + entities: { + deployedDevices: { $set: deployedDevices } + }, + ...setPending(fromAction.type, false) + }); +}; + /* Action types that cause a pending flag */ const fetchableTypes = [ + epics.actionTypes.fetchDeployment, epics.actionTypes.fetchDeployments, epics.actionTypes.createDeployment, - epics.actionTypes.deleteDeployment + epics.actionTypes.deleteDeployment, + epics.actionTypes.fetchDeployedDevices ]; export const redux = createReducerScenario({ insertDeployment: { type: 'DEPLOYMENT_INSERT', reducer: insertDeploymentReducer }, deleteDeployment: { type: 'DEPLOYMENTS_DELETE', reducer: deleteDeploymentReducer }, updateDeployments: { type: 'DEPLOYMENTS_UPDATE', reducer: updateDeploymentsReducer }, + updateDeployment: { type: 'DEPLOYMENTS_DETAILS_UPDATE', reducer: updateDeploymentReducer }, + updateDeployedDevices: { type: 'DEPLOYED_DEVICES_UPDATE', reducer: updateDeployedDevicesReducer }, registerError: { type: 'DEPLOYMENTS_REDUCER_ERROR', reducer: errorReducer }, resetPendingAndError: { type: 'DEPLOYMENTS_REDUCER_RESET_ERROR_PENDING', reducer: resetPendingAndErrorReducer }, isFetching: { multiType: fetchableTypes, reducer: pendingReducer } @@ -118,6 +186,7 @@ export const reducer = { deployments: redux.getReducer(initialState) }; // ========================= Selectors - START export const getDeploymentsReducer = state => state.deployments; export const getEntities = state => getDeploymentsReducer(state).entities || {}; +export const getDeploymentsEntities = state => getEntities(state).deployments || {}; export const getItems = state => getDeploymentsReducer(state).items || []; export const getDeploymentsLastUpdated = state => getDeploymentsReducer(state).lastUpdated; export const getDeploymentsError = state => @@ -133,14 +202,37 @@ export const getDeleteDeploymentError = state => export const getDeleteDeploymentPendingStatus = state => getPending(getDeploymentsReducer(state), epics.actionTypes.deleteDeployment); export const getDeployments = createSelector( - getEntities, getItems, getActiveDeviceGroupId, getActiveDeviceGroupConditions, - (entities, items, deviceGroupId, deviceGroupConditions = []) => + getDeploymentsEntities, getItems, getActiveDeviceGroupId, getActiveDeviceGroupConditions, + (deployments, items, deviceGroupId, deviceGroupConditions = []) => items.reduce((acc, id) => { - const deployment = entities[id]; + const deployment = deployments[id]; const activeDeviceGroup = deviceGroupConditions.length > 0 ? deviceGroupId : false; - return (deployment.deviceGroupId === activeDeviceGroup || !activeDeviceGroup) + return ((deployment && deployment.deviceGroupId && deployment.deviceGroupId === activeDeviceGroup) || !activeDeviceGroup) ? [...acc, deployment] : acc }, []) ); +export const getCurrentDeploymentDetails = state => getDeploymentsReducer(state).currentDeployment || {}; +export const getCurrentDeploymentLastUpdated = state => getDeploymentsReducer(state).currentDeploymentLastUpdated; +export const getDeviceStatuses = state => getDeploymentsReducer(state).deviceStatuses || {}; +export const getCurrentDeploymentDetailsPendingStatus = state => + getPending(getDeploymentsReducer(state), epics.actionTypes.fetchDeployment); +export const getCurrentDeploymentDetailsError = state => + getError(getDeploymentsReducer(state), epics.actionTypes.fetchDeployment); +export const getDeployedDevicesEntities = state => getEntities(state).deployedDevices || {}; +export const getDeployedDevicesPendingStatus = state => + getPending(getDeploymentsReducer(state), epics.actionTypes.fetchDeployedDevices); +export const getDeployedDevicesError = state => + getError(getDeploymentsReducer(state), epics.actionTypes.fetchDeployedDevices); +export const getDeployedDevices = createSelector( + getDeployedDevicesEntities, getDeviceStatuses, + (DeployedDevicesEntities, deviceStatuses) => + Object.values(Object.keys(deviceStatuses).reduce((acc, deviceId) => ({ + ...acc, + [deviceId]: { + ...(acc[deviceId] || {}), + deploymentStatus: deviceStatuses[deviceId] + } + }), DeployedDevicesEntities)) +); // ========================= Selectors - END diff --git a/src/styles/_themes.scss b/src/styles/_themes.scss index 640684fbf..59b0cd91c 100644 --- a/src/styles/_themes.scss +++ b/src/styles/_themes.scss @@ -8,6 +8,7 @@ $themes: ( dark: ( // Functional colors colorAlert: $colorAlert, + colorFailed: $colorFailed, colorWarning: $colorWarning, colorSystem: $colorSystem, colorError: $colorError, diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 2bd4615cf..a9797ce96 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -38,6 +38,7 @@ $colorLightLinkDisabled: #A6A6A6; // Function color variables $colorAlert: #fc540a; +$colorFailed: #FF2626; $colorWarning: #ffee91; $colorSystem: #7065fd; $colorError: #EA692D; diff --git a/src/utilities/svgs.js b/src/utilities/svgs.js index 57cce81e7..28b387749 100644 --- a/src/utilities/svgs.js +++ b/src/utilities/svgs.js @@ -25,6 +25,7 @@ import EditIconPath from 'assets/icons/edit.svg'; import EllipsisIconPath from 'assets/icons/ellipsis.svg'; import EnableToggleIconPath from 'assets/icons/enableToggle.svg'; import ErrorIconPath from 'assets/icons/errorAsterisk.svg'; +import FailedIconPath from 'assets/icons/failed.svg'; import GlimmerIconPath from 'assets/icons/glimmer.svg'; import HamburgerIconPath from 'assets/icons/hamburger.svg'; import InfoBubbleIconPath from 'assets/icons/infoBubble.svg'; @@ -94,6 +95,7 @@ export const svgs = { ellipsis: EllipsisIconPath, enableToggle: EnableToggleIconPath, error: ErrorIconPath, + failed: FailedIconPath, glimmer: GlimmerIconPath, hamburger: HamburgerIconPath, info: InfoIconPath, From 028f89ed3a72f04c06e61c16964b4ef07eab62ef Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 2 Oct 2018 10:30:57 -0700 Subject: [PATCH 20/25] Delete Modal for Packages and Deployments Page (#1115) * delete Packages and delete Deployments * remove deleteDeployment.container * remove componentArray from deploymentDetailsGrid * remove payload * onrowclicked and softselected for deviceDetails * remove comment * id = '' --- public/locales/en/translations.json | 26 ++++--- .../deploymentDetails.container.js | 9 ++- .../deploymentDetails/deploymentDetails.js | 46 ++++++++++++- .../deploymentDetails.test.js | 1 + .../deploymentDetailsGrid.js | 68 +++++++++++++++++-- .../deploymentDetailsGridConfig.js | 4 +- .../deploymentsHome/deployments.js | 2 +- .../flyouts/deploymentNew/deploymentNew.js | 2 +- .../pages/devices/devicesGrid/devicesGrid.js | 2 +- .../pages/packages/flyouts/index.js | 1 - .../packages/flyouts/packageDelete/index.js | 4 -- .../deletePackage/deletePackage.container.js} | 6 +- src/components/pages/packages/modals/index.js | 3 + .../pages/packages/packages.container.js | 3 +- src/components/pages/packages/packages.js | 2 +- .../pages/packages/packages.test.js | 5 +- .../packages/packagesGrid/packagesGrid.js | 29 ++++---- .../pages/rules/rulesGrid/rulesGrid.js | 2 +- .../deleteModal/deleteModal.js} | 27 ++++---- .../deleteModal/deleteModal.scss} | 2 +- src/components/shared/index.js | 1 + src/services/iotHubManagerService.js | 2 +- src/services/models/authModels.js | 5 +- src/store/reducers/deploymentsReducer.js | 13 ++-- .../pageWithGrid/exampleGrid/exampleGrid.js | 2 +- 25 files changed, 192 insertions(+), 75 deletions(-) delete mode 100644 src/components/pages/packages/flyouts/packageDelete/index.js rename src/components/pages/packages/{flyouts/packageDelete/packageDelete.container.js => modals/deletePackage/deletePackage.container.js} (75%) create mode 100644 src/components/pages/packages/modals/index.js rename src/components/{pages/packages/flyouts/packageDelete/packageDelete.js => shared/deleteModal/deleteModal.js} (59%) rename src/components/{pages/packages/flyouts/packageDelete/packageDelete.scss => shared/deleteModal/deleteModal.scss} (96%) diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index a44b7d21d..fba321166 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -566,19 +566,12 @@ "namePlaceHolder": "Enter name", "targetText": "targeted devices", "infoText": "* This deployment runs continuously. Every edge device (and any you add in the future) in the selected device group will receive this package.", - "successText": "View your deployment status detail for {{deplymentName}}", + "successText": "View your deployment status detail for {{deploymentName}}", "creating": "Creating deployment", "validation": { "required": "Is required", "nan": "Must be a number" } - }, - "delete": { - "contextMenuName": "Delete", - "title": "Delete Deployment?", - "delete": "Delete", - "cancel": "Cancel", - "info": "Deleting {deploymentName} will stop this deployment from being applied to these devices. It may result in a lower priority deployment being applied" } }, "details": { @@ -602,6 +595,13 @@ "end": "End" } }, + "modals": { + "delete": { + "contextMenuName": "Delete", + "title": "Delete Deployment?", + "info": "Deleting '{{deploymentName}}' will stop this deployment from being applied to these devices. It may result in a lower priority edge manifest being applied." + } + }, "typeOptions": { "edgemanifest": "Edge Manifest" }, @@ -646,11 +646,11 @@ "validation": { "required": "Is required" } - }, + } + }, + "modals": { "delete": { "title": "Delete Package?", - "delete": "Delete", - "cancel": "Cancel", "info": "Deleting selected package will remove it. It will not impact any of the deployments of this package." } }, @@ -658,6 +658,10 @@ "edgemanifest": "Edge Manifest" } }, + "modal": { + "delete": "Delete", + "cancel": "Cancel" + }, "walkthrough": { "tabs": { "dashboard": "Dashboard", diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js index d0ebcdd61..7d2fcfadc 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js @@ -11,6 +11,8 @@ import { getDeployedDevicesPendingStatus, getDeployedDevicesError, getDeployedDevices, + getDeleteDeploymentError, + getDeleteDeploymentPendingStatus, epics as deploymentsEpics } from 'store/reducers/deploymentsReducer'; @@ -22,12 +24,15 @@ const mapStateToProps = state => ({ isDeployedDevicesPending: getDeployedDevicesPendingStatus(state), deployedDevicesError: getDeployedDevicesError(state), deployedDevices: getDeployedDevices(state), - lastUpdated: getCurrentDeploymentLastUpdated(state) + lastUpdated: getCurrentDeploymentLastUpdated(state), + deleteIsPending: getDeleteDeploymentPendingStatus(state), + deleteError: getDeleteDeploymentError(state) }); // Wrap the dispatch methods const mapDispatchToProps = dispatch => ({ - fetchDeployment: (id) => dispatch(deploymentsEpics.actions.fetchDeployment(id)) + fetchDeployment: id => dispatch(deploymentsEpics.actions.fetchDeployment(id)), + deleteItem: deploymentId => dispatch(deploymentsEpics.actions.deleteDeployment(deploymentId)) }); export const DeploymentDetailsContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeploymentDetails)); diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js index a792f606a..ec5d70145 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js @@ -8,6 +8,7 @@ import { ComponentArray, ContextMenu, ContextMenuAlign, + DeleteModal, Indicator, PageContent, Protected, @@ -22,12 +23,51 @@ import { DeploymentDetailsGrid } from './deploymentDetailsGrid/deploymentDetails import "./deploymentDetails.css"; +const closedModalState = { + openModalName: undefined +}; + export class DeploymentDetails extends Component { constructor(props) { super(props); + // Set the initial state + this.state = { + ...closedModalState, + deploymentDeleted: false + }; props.fetchDeployment(props.match.params.id); } + getOpenModal = () => { + const { t, deleteIsPending, deleteError, deleteItem } = this.props; + if (this.state.openModalName === 'delete-deployment' && this.props.currentDeployment) { + return + } + return null; + } + + openModal = (modalName) => () => this.setState({ + openModalName: modalName + }); + + closeModal = () => this.setState(closedModalState); + + onDelete = () => { + this.closeModal(); + this.props.history.push(`/deployments`) + } + render() { const { t, @@ -56,16 +96,20 @@ export class DeploymentDetails extends Component { return ( + {this.getOpenModal()} - {t('deployments.flyouts.delete.contextMenuName')} + {t('deployments.modals.delete.contextMenuName')} + + {!!error && } + {isPending && } { diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js index 927504cf0..7d7b1bb2c 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js @@ -13,6 +13,7 @@ describe('Deployment details Component', () => { t: () => { }, match: { params: { id: 'testId' } }, fetchDeployment: () => { }, + deleteItem: () => { }, currentDeployment: {} }; diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js index 7af7034fe..718fe7ecd 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js @@ -2,13 +2,23 @@ import React, { Component } from 'react'; import { deploymentDetailsColumnDefs, defaultDeploymentDetailsGridProps } from './deploymentDetailsGridConfig'; -import { translateColumnDefs } from 'utilities'; -import { PcsGrid } from 'components/shared'; +import { translateColumnDefs, isFunc } from 'utilities'; +import { PcsGrid, ComponentArray } from 'components/shared'; +import { DeviceDetailsContainer } from 'components/pages/devices/flyouts'; + +const closedFlyoutState = { + openFlyoutName: undefined, + selectedDevice: undefined +}; export class DeploymentDetailsGrid extends Component { constructor(props) { super(props); + this.state = { + ...closedFlyoutState + }; + this.columnDefs = [ deploymentDetailsColumnDefs.name, deploymentDetailsColumnDefs.deploymentStatus, @@ -19,7 +29,43 @@ export class DeploymentDetailsGrid extends Component { ]; } - onGridReady = gridReadyEvent => this.deployedDevicesGridApi = gridReadyEvent.api; + onGridReady = gridReadyEvent => { + this.deployedDevicesGridApi = gridReadyEvent.api; + // Call the onReady props if it exists + if (isFunc(this.props.onGridReady)) { + this.props.onGridReady(gridReadyEvent); + } + }; + + onSoftSelectChange = (deviceRowId, rowEvent) => { + const { onSoftSelectChange } = this.props; + const rowData = (this.deployedDevicesGridApi.getDisplayedRowAtIndex(deviceRowId) || {}).data; + if (rowData && rowData.device) { + this.setState({ + openFlyoutName: 'deviceDetails', + selectedDevice: rowData.device + }); + } else { + this.closeFlyout(); + } + if (isFunc(onSoftSelectChange)) { + onSoftSelectChange(rowData, rowEvent); + } + } + + closeFlyout = () => this.setState(closedFlyoutState); + + getSoftSelectId = ({ id } = '') => id; + + onRowClicked = (node, data) => { + node.setSelected(!node.isSelected()); + if (data && data.device) { + this.setState({ + openFlyoutName: 'deviceDetails', + selectedDevice: data.device + }); + } + } render() { const gridProps = { @@ -28,9 +74,21 @@ export class DeploymentDetailsGrid extends Component { t: this.props.t, rowData: this.props.deployedDevices, getRowNodeId: ({ id }) => id, - columnDefs: translateColumnDefs(this.props.t, this.columnDefs) + columnDefs: translateColumnDefs(this.props.t, this.columnDefs), + onGridReady: this.onGridReady, + getSoftSelectId: this.getSoftSelectId, + onSoftSelectChange: this.onSoftSelectChange, + onRowClicked: ({ node, data }) => this.onRowClicked(node, data) }; - return (); + return ( + + + { + this.state.openFlyoutName === 'deviceDetails' && + + } + + ); } } diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js index 8caa21bfc..52faed391 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import Config from 'app.config'; -import { TimeRenderer } from 'components/shared/cellRenderers'; +import { TimeRenderer, SoftSelectLinkRenderer } from 'components/shared/cellRenderers'; import { gridValueFormatters } from 'components/shared/pcsGrid/pcsGridConfig'; const { checkForEmpty } = gridValueFormatters; @@ -11,7 +11,7 @@ export const deploymentDetailsColumnDefs = { headerName: 'deployments.details.grid.name', field: 'id', sort: 'asc', - valueFormatter: ({ value }) => checkForEmpty(value) + cellRendererFramework: SoftSelectLinkRenderer }, deploymentStatus: { headerName: 'deployments.details.grid.deploymentStatus', diff --git a/src/components/pages/deployments/deploymentsHome/deployments.js b/src/components/pages/deployments/deploymentsHome/deployments.js index cb47228e8..5e8e110bc 100644 --- a/src/components/pages/deployments/deploymentsHome/deployments.js +++ b/src/components/pages/deployments/deploymentsHome/deployments.js @@ -79,7 +79,7 @@ export class Deployments extends Component { {this.state.contextBtns} - + {t('deployments.flyouts.new.contextMenuName')} diff --git a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js index a725daa09..5c2337a22 100644 --- a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js +++ b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js @@ -294,7 +294,7 @@ export class DeploymentNew extends LinkedComponent { {/** Displays a success message if deployment is created successfully */ completedSuccessfully &&
- {t('deployments.flyouts.new.successText', { deplymentName: name })} + {t('deployments.flyouts.new.successText', { deploymentName: name })}
} {/** Displays an error message if one occurs while creating deployment. */ diff --git a/src/components/pages/devices/devicesGrid/devicesGrid.js b/src/components/pages/devices/devicesGrid/devicesGrid.js index 0da27feed..9522e5c43 100644 --- a/src/components/pages/devices/devicesGrid/devicesGrid.js +++ b/src/components/pages/devices/devicesGrid/devicesGrid.js @@ -120,7 +120,7 @@ export class DevicesGrid extends Component { } } - getSoftSelectId = ({ id } = {}) => id; + getSoftSelectId = ({ id } = '') => id; render() { const gridProps = { diff --git a/src/components/pages/packages/flyouts/index.js b/src/components/pages/packages/flyouts/index.js index e8390b0d6..964310060 100644 --- a/src/components/pages/packages/flyouts/index.js +++ b/src/components/pages/packages/flyouts/index.js @@ -1,4 +1,3 @@ // Copyright (c) Microsoft. All rights reserved. export * from './packageNew'; -export * from './packageDelete'; diff --git a/src/components/pages/packages/flyouts/packageDelete/index.js b/src/components/pages/packages/flyouts/packageDelete/index.js deleted file mode 100644 index 70e24381b..000000000 --- a/src/components/pages/packages/flyouts/packageDelete/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -export * from './packageDelete'; -export * from './packageDelete.container'; diff --git a/src/components/pages/packages/flyouts/packageDelete/packageDelete.container.js b/src/components/pages/packages/modals/deletePackage/deletePackage.container.js similarity index 75% rename from src/components/pages/packages/flyouts/packageDelete/packageDelete.container.js rename to src/components/pages/packages/modals/deletePackage/deletePackage.container.js index 89376e544..a55ce11c8 100644 --- a/src/components/pages/packages/flyouts/packageDelete/packageDelete.container.js +++ b/src/components/pages/packages/modals/deletePackage/deletePackage.container.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { translate } from 'react-i18next'; -import { PackageDelete } from './packageDelete'; +import { DeleteModal } from 'components/shared'; import { getDeletePackageError, getDeletePackagePendingStatus, @@ -17,7 +17,7 @@ const mapStateToProps = state => ({ // Wrap the dispatch methods const mapDispatchToProps = dispatch => ({ - deletePackage: packageId => dispatch(packagesEpics.actions.deletePackage(packageId)) + deleteItem: packageId => dispatch(packagesEpics.actions.deletePackage(packageId)) }); -export const PackageDeleteContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(PackageDelete)); +export const PackageDeleteContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeleteModal)); diff --git a/src/components/pages/packages/modals/index.js b/src/components/pages/packages/modals/index.js new file mode 100644 index 000000000..40e172af1 --- /dev/null +++ b/src/components/pages/packages/modals/index.js @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './deletePackage/deletePackage.container'; diff --git a/src/components/pages/packages/packages.container.js b/src/components/pages/packages/packages.container.js index f6c59bb8a..66fe57382 100644 --- a/src/components/pages/packages/packages.container.js +++ b/src/components/pages/packages/packages.container.js @@ -21,8 +21,7 @@ const mapStateToProps = state => ({ // Wrap the dispatch method const mapDispatchToProps = dispatch => ({ - fetchPackages: () => dispatch(packagesEpics.actions.fetchPackages()), - deletePackages: (packageIdArr) => dispatch(packagesEpics.actions.deletePackages(packageIdArr)) + fetchPackages: () => dispatch(packagesEpics.actions.fetchPackages()) }); export const PackagesContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Packages)); diff --git a/src/components/pages/packages/packages.js b/src/components/pages/packages/packages.js index 93afb2e72..67b600f44 100644 --- a/src/components/pages/packages/packages.js +++ b/src/components/pages/packages/packages.js @@ -71,7 +71,7 @@ export class Packages extends Component { {this.state.contextBtns} - + {t('packages.new')} diff --git a/src/components/pages/packages/packages.test.js b/src/components/pages/packages/packages.test.js index c55a182fe..8e42e6367 100644 --- a/src/components/pages/packages/packages.test.js +++ b/src/components/pages/packages/packages.test.js @@ -16,9 +16,8 @@ describe('Packages Component', () => { error: undefined, isPending: false, lastUpdated: undefined, - fetchPackages: () => {}, - deletePackages: (p) => {}, - t: () => {}, + fetchPackages: () => { }, + t: () => { }, }; const wrapper = shallow( diff --git a/src/components/pages/packages/packagesGrid/packagesGrid.js b/src/components/pages/packages/packagesGrid/packagesGrid.js index 005cac7ce..2c75c5cae 100644 --- a/src/components/pages/packages/packagesGrid/packagesGrid.js +++ b/src/components/pages/packages/packagesGrid/packagesGrid.js @@ -5,12 +5,12 @@ import { packagesColumnDefs, defaultPackagesGridProps } from './packagesGridConf import { Btn, ComponentArray, PcsGrid, Protected } from 'components/shared'; import { isFunc, translateColumnDefs, svgs } from 'utilities'; import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig'; -import { PackageDeleteContainer } from '../flyouts'; +import { PackageDeleteContainer } from '../modals'; import './packagesGrid.css'; -const closedFlyoutState = { - openFlyoutName: undefined +const closedModalState = { + openModalName: undefined }; export class PackagesGrid extends Component { @@ -19,7 +19,7 @@ export class PackagesGrid extends Component { // Set the initial state this.state = { - ...closedFlyoutState, + ...closedModalState, hardSelectedPackages: [] }; @@ -33,14 +33,19 @@ export class PackagesGrid extends Component { this.contextBtns = - {props.t('packages.delete')} + {props.t('packages.delete')} ; } - getOpenFlyout = () => { - if (this.state.openFlyoutName === 'delete-package') { - return + getOpenModal = () => { + if (this.state.openModalName === 'delete-package' && this.state.hardSelectedPackages[0]) { + return } return null; } @@ -76,10 +81,10 @@ export class PackagesGrid extends Component { } } - closeFlyout = () => this.setState(closedFlyoutState); + closeModal = () => this.setState(closedModalState); - openFlyout = (flyoutName) => () => this.setState({ - openFlyoutName: flyoutName + openModal = (modalName) => () => this.setState({ + openModalName: modalName }); render() { @@ -103,7 +108,7 @@ export class PackagesGrid extends Component { return ( - {this.getOpenFlyout()} + {this.getOpenModal()} ); } diff --git a/src/components/pages/rules/rulesGrid/rulesGrid.js b/src/components/pages/rules/rulesGrid/rulesGrid.js index b65d689c0..c3c657038 100644 --- a/src/components/pages/rules/rulesGrid/rulesGrid.js +++ b/src/components/pages/rules/rulesGrid/rulesGrid.js @@ -175,7 +175,7 @@ export class RulesGrid extends Component { } } - getSoftSelectId = ({ id } = {}) => id; + getSoftSelectId = ({ id } = '') => id; closeFlyout = () => this.setState(closedFlyoutState); diff --git a/src/components/pages/packages/flyouts/packageDelete/packageDelete.js b/src/components/shared/deleteModal/deleteModal.js similarity index 59% rename from src/components/pages/packages/flyouts/packageDelete/packageDelete.js rename to src/components/shared/deleteModal/deleteModal.js index 3a814f243..83d2f73cb 100644 --- a/src/components/pages/packages/flyouts/packageDelete/packageDelete.js +++ b/src/components/shared/deleteModal/deleteModal.js @@ -1,8 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -import React from 'react'; +import React, { Component } from 'react'; -import { LinkedComponent } from 'utilities'; import { AjaxError, Btn, @@ -12,9 +11,9 @@ import { } from 'components/shared'; import { svgs } from 'utilities'; -import './packageDelete.css'; +import './deleteModal.css'; -export class PackageDelete extends LinkedComponent { +export class DeleteModal extends Component { constructor(props) { super(props); @@ -24,36 +23,36 @@ export class PackageDelete extends LinkedComponent { }; } - componentWillReceiveProps({ error, isPending, onClose }) { + componentWillReceiveProps({ error, isPending, onDelete }) { if (this.state.changesApplied && !error && !isPending) { - onClose(); + onDelete(); } } apply = () => { - const { deletePackage, package: { id } } = this.props; - deletePackage(id); + const { deleteItem, itemId } = this.props; + deleteItem(itemId); this.setState({ changesApplied: true }); } render() { - const { t, onClose, isPending, error } = this.props; + const { t, onClose, isPending, error, title, deleteInfo } = this.props; const { changesApplied } = this.state; return ( - +
-
{t('packages.flyouts.delete.title')}
+
{title}
- {t('packages.flyouts.delete.info')} + {deleteInfo}
{ !changesApplied && - {t('packages.flyouts.delete.delete')} - {t('packages.flyouts.delete.cancel')} + {t('modal.delete')} + {t('modal.cancel')} } {isPending && } diff --git a/src/components/pages/packages/flyouts/packageDelete/packageDelete.scss b/src/components/shared/deleteModal/deleteModal.scss similarity index 96% rename from src/components/pages/packages/flyouts/packageDelete/packageDelete.scss rename to src/components/shared/deleteModal/deleteModal.scss index 91da7db2a..5e8ce3b59 100644 --- a/src/components/pages/packages/flyouts/packageDelete/packageDelete.scss +++ b/src/components/shared/deleteModal/deleteModal.scss @@ -4,7 +4,7 @@ @import 'src/styles/mixins'; @import 'src/styles/themes'; -.delete-package-container { +.delete-modal-container { @include rem-fallback(font-size, 14px); .delete-header-container { diff --git a/src/components/shared/index.js b/src/components/shared/index.js index 320fac53a..a62416776 100644 --- a/src/components/shared/index.js +++ b/src/components/shared/index.js @@ -17,3 +17,4 @@ export * from './protected' export * from './refreshBar/refreshBar'; export * from './svg/svg'; export * from './modal'; +export * from './deleteModal/deleteModal'; diff --git a/src/services/iotHubManagerService.js b/src/services/iotHubManagerService.js index b4d3ae0ea..b51d62662 100644 --- a/src/services/iotHubManagerService.js +++ b/src/services/iotHubManagerService.js @@ -101,7 +101,7 @@ export class IoTHubManagerService { /** Delete a deployment */ static deleteDeployment(id) { - return HttpClient.delete(`${ENDPOINT}deployments${id}`) + return HttpClient.delete(`${ENDPOINT}deployments/${id}`) .map(() => id); } diff --git a/src/services/models/authModels.js b/src/services/models/authModels.js index 4ba62396b..ff2bc9507 100644 --- a/src/services/models/authModels.js +++ b/src/services/models/authModels.js @@ -24,7 +24,10 @@ export const permissions = { updateSIMManagement: 'UpdateSIMManagement', deletePackages: 'DeletePackages', - addPackages: 'AddPackages' + createPackages: 'CreatePackages', + + createDeployments: 'CreateDeployments', + delteDeployments: 'DeleteDeployments' }; export const toUserModel = (user = {}) => camelCaseReshape(user, { diff --git a/src/store/reducers/deploymentsReducer.js b/src/store/reducers/deploymentsReducer.js index ff71f9a9a..7af4d1f3a 100644 --- a/src/store/reducers/deploymentsReducer.js +++ b/src/store/reducers/deploymentsReducer.js @@ -109,10 +109,10 @@ const insertDeploymentReducer = (state, { payload, fromAction }) => { }); }; -const deleteDeploymentReducer = (state, { payload, fromAction }) => { - const idx = state.items.indexOf(payload); +const deleteDeploymentReducer = (state, { fromAction }) => { + const idx = state.items.indexOf(fromAction.payload); return update(state, { - entities: { deployments: { $unset: [payload] } }, + entities: { deployments: { $unset: [fromAction.payload] } }, items: { $splice: [[idx, 1]] }, ...setPending(fromAction.type, false) }); @@ -139,15 +139,16 @@ const updateDeploymentReducer = (state, { payload, fromAction }) => { }; const updateDeployedDevicesReducer = (state, { payload: [modules, devices], fromAction }) => { - const normalizedDevices = normalize(devices, deployedDevicesListSchema).entities.deployedDevices; - const normalizedModules = normalize(modules, deployedDevicesListSchema).entities.deployedDevices; + const normalizedDevices = normalize(devices, deployedDevicesListSchema).entities.deployedDevices || {}; + const normalizedModules = normalize(modules, deployedDevicesListSchema).entities.deployedDevices || {}; const deployedDevices = Object.keys(normalizedDevices) .reduce( (acc, deviceId) => ({ ...acc, [deviceId]: { ...(acc[deviceId] || {}), - firmware: normalizedDevices[deviceId].firmware + firmware: normalizedDevices[deviceId].firmware, + device: normalizedDevices[deviceId] } }), normalizedModules diff --git a/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js b/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js index 6c110bfa9..578bd56d6 100644 --- a/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js +++ b/src/walkthrough/components/pages/pageWithGrid/exampleGrid/exampleGrid.js @@ -93,7 +93,7 @@ export class ExampleGrid extends Component { } } - getSoftSelectId = ({ id } = {}) => id; + getSoftSelectId = ({ id } = '') => id; render() { const gridProps = { From 612b72befab47559bfd2e9bf62cdebad5f588fc4 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 4 Oct 2018 18:05:33 -0700 Subject: [PATCH 21/25] Adding Deployment message details formatting and removing onRowClick (#1117) * Deployment message details formatting and removing onRowClick * fix for stephen's comments * minimize setState calls * refactoring componentWillReceiveProps * semicolon and single line --- public/locales/en/translations.json | 14 +++ .../deploymentDetails.container.js | 4 +- .../deploymentDetails/deploymentDetails.js | 4 + .../deploymentDetailsGrid.js | 33 +++---- .../deploymentDetailsGridConfig.js | 5 +- .../deploymentsHome/deployments.js | 12 ++- .../deploymentsGrid/deploymentsGrid.js | 4 - .../deploymentsGrid/deploymentsGridConfig.js | 6 +- .../deviceDetails/deviceDetails.container.js | 14 ++- .../flyouts/deviceDetails/deviceDetails.js | 96 +++++++++++++++++-- .../flyouts/deviceDetails/deviceDetails.scss | 9 ++ src/services/iotHubManagerService.js | 2 +- src/services/models/iotHubManagerModels.js | 3 +- src/store/reducers/deploymentsReducer.js | 10 +- src/store/reducers/devicesReducer.js | 39 +++++++- src/utilities/methods.js | 13 +++ 16 files changed, 230 insertions(+), 38 deletions(-) diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index fba321166..a6c54905b 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -21,6 +21,15 @@ "deployments": "Deployments" }, "errorFormat": "Error: {{message}}", + "edgeAgentStatus": { + "200": "OK.", + "400": "The deployment configuration is malformed or invalid.", + "406": "The device is offline or not sending status reports.", + "412": "The deployment configuration schema version is invalid.", + "417": "The device's deployment configuration is not set.", + "500": "An error occurred in the IoT Edge runtime.", + "unknown": "An unknown code received." + }, "errorCode": { "noResponse": "Oops, there was no response from the server.", "notLoggedIn": "You need to login to call the service.", @@ -209,6 +218,11 @@ "description": "To run a method on one or more devices, close this pane, select the checkbox for the device(s), click <1><0>{{jobs}}, and then select <3><0>{{methods}}.", "noneExist": "No methods found for this device." }, + "modules": { + "title": "Deployment messages", + "description": "Edge module messages on the device", + "noneExist": "No messages found for this device" + }, "properties": { "title": "Properties", "description": "To change a property on one or more devices, close this pane, select the checkbox for the device(s), click <1><0>{{jobs}}, and then select <3><0>{{properties}}.", diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js index 7d2fcfadc..6e49272ad 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js @@ -13,7 +13,8 @@ import { getDeployedDevices, getDeleteDeploymentError, getDeleteDeploymentPendingStatus, - epics as deploymentsEpics + epics as deploymentsEpics, + redux as deploymentsRedux } from 'store/reducers/deploymentsReducer'; // Pass the global info needed @@ -32,6 +33,7 @@ const mapStateToProps = state => ({ // Wrap the dispatch methods const mapDispatchToProps = dispatch => ({ fetchDeployment: id => dispatch(deploymentsEpics.actions.fetchDeployment(id)), + resetDeployedDevices: () => dispatch(deploymentsRedux.actions.resetDeployedDevices()), deleteItem: deploymentId => dispatch(deploymentsEpics.actions.deleteDeployment(deploymentId)) }); diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js index ec5d70145..f6a4cb127 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js @@ -38,6 +38,10 @@ export class DeploymentDetails extends Component { props.fetchDeployment(props.match.params.id); } + componentWillUnmount() { + this.props.resetDeployedDevices(); + } + getOpenModal = () => { const { t, deleteIsPending, deleteError, deleteItem } = this.props; if (this.state.openModalName === 'delete-deployment' && this.props.currentDeployment) { diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js index 718fe7ecd..3d3ced3d8 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js @@ -37,13 +37,19 @@ export class DeploymentDetailsGrid extends Component { } }; + getModuleStatus = data => ({ + code: data.code, + description: data.description + }); + onSoftSelectChange = (deviceRowId, rowEvent) => { const { onSoftSelectChange } = this.props; const rowData = (this.deployedDevicesGridApi.getDisplayedRowAtIndex(deviceRowId) || {}).data; if (rowData && rowData.device) { this.setState({ openFlyoutName: 'deviceDetails', - selectedDevice: rowData.device + selectedDevice: rowData.device, + moduleStatus: this.getModuleStatus(rowData) }); } else { this.closeFlyout(); @@ -55,30 +61,21 @@ export class DeploymentDetailsGrid extends Component { closeFlyout = () => this.setState(closedFlyoutState); - getSoftSelectId = ({ id } = '') => id; - - onRowClicked = (node, data) => { - node.setSelected(!node.isSelected()); - if (data && data.device) { - this.setState({ - openFlyoutName: 'deviceDetails', - selectedDevice: data.device - }); - } - } + getSoftSelectId = ({ id = '' }) => id; render() { const gridProps = { /* Grid Properties */ ...defaultDeploymentDetailsGridProps, - t: this.props.t, + context: { + t: this.props.t, + }, rowData: this.props.deployedDevices, getRowNodeId: ({ id }) => id, columnDefs: translateColumnDefs(this.props.t, this.columnDefs), onGridReady: this.onGridReady, getSoftSelectId: this.getSoftSelectId, - onSoftSelectChange: this.onSoftSelectChange, - onRowClicked: ({ node, data }) => this.onRowClicked(node, data) + onSoftSelectChange: this.onSoftSelectChange }; return ( @@ -86,7 +83,11 @@ export class DeploymentDetailsGrid extends Component { { this.state.openFlyoutName === 'deviceDetails' && - + } ); diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js index 52faed391..9fa3ef42d 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGridConfig.js @@ -2,6 +2,7 @@ import Config from 'app.config'; import { TimeRenderer, SoftSelectLinkRenderer } from 'components/shared/cellRenderers'; +import { getEdgeAgentStatusCode } from 'utilities'; import { gridValueFormatters } from 'components/shared/pcsGrid/pcsGridConfig'; const { checkForEmpty } = gridValueFormatters; @@ -25,8 +26,8 @@ export const deploymentDetailsColumnDefs = { }, lastMessage: { headerName: 'deployments.details.grid.lastMessage', - field: 'lastMessage', - valueFormatter: ({ value }) => checkForEmpty(value) + field: 'code', + valueFormatter: ({ value, context: { t } }) => getEdgeAgentStatusCode(value, t) }, start: { headerName: 'deployments.details.grid.start', diff --git a/src/components/pages/deployments/deploymentsHome/deployments.js b/src/components/pages/deployments/deploymentsHome/deployments.js index 5e8e110bc..e3d27ddf9 100644 --- a/src/components/pages/deployments/deploymentsHome/deployments.js +++ b/src/components/pages/deployments/deploymentsHome/deployments.js @@ -57,15 +57,23 @@ export class Deployments extends Component { onGridReady = gridReadyEvent => this.deploymentGridApi = gridReadyEvent.api; + getSoftSelectId = ({ id } = '') => id; + + onSoftSelectChange = (deviceRowId) => { + const rowData = (this.deploymentGridApi.getDisplayedRowAtIndex(deviceRowId) || {}).data; + this.props.history.push(`/deployments/${rowData.id}`) + } + render() { - const { t, deployments, error, isPending, fetchDeployments, lastUpdated, history } = this.props; + const { t, deployments, error, isPending, fetchDeployments, lastUpdated } = this.props; const gridProps = { onGridReady: this.onGridReady, rowData: isPending ? undefined : deployments || [], refresh: fetchDeployments, onContextMenuChange: this.onContextMenuChange, t: t, - onRowClicked: ({ data: { id } }) => history.push(`/deployments/${id}`) + getSoftSelectId: this.getSoftSelectId, + onSoftSelectChange: this.onSoftSelectChange }; return ( diff --git a/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGrid.js b/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGrid.js index 2ee516f7a..4c30f27c1 100644 --- a/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGrid.js +++ b/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGrid.js @@ -38,13 +38,9 @@ export class DeploymentsGrid extends Component { /* Grid Properties */ ...defaultDeploymentsGridProps, columnDefs: translateColumnDefs(this.props.t, this.columnDefs), - sizeColumnsToFit: true, - deltaRowDataMode: true, ...this.props, // Allow default property overrides onGridReady: event => this.onGridReady(event), // Wrap in a function to avoid closure issues getRowNodeId: ({ id }) => id, - enableSorting: true, - unSortIcon: true, context: { t: this.props.t } diff --git a/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js b/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js index 1b8865be8..9f0fc86f9 100644 --- a/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js +++ b/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js @@ -59,5 +59,9 @@ export const deploymentsColumnDefs = { export const defaultDeploymentsGridProps = { enableColResize: true, pagination: true, - paginationPageSize: Config.paginationPageSize + paginationPageSize: Config.paginationPageSize, + enableSorting: true, + unSortIcon: true, + sizeColumnsToFit: true, + deltaRowDataMode: true }; diff --git a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.container.js b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.container.js index 7a3f6e00f..144512b45 100644 --- a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.container.js +++ b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.container.js @@ -10,6 +10,13 @@ import { getRulesLastUpdated, getRulesPendingStatus } from 'store/reducers/rulesReducer'; +import { + getDeviceModuleStatus, + getDeviceModuleStatusPendingStatus, + getDeviceModuleStatusError, + epics as devicesEpics, + redux as devicesRedux +} from 'store/reducers/devicesReducer'; // Pass the device details const mapStateToProps = state => ({ @@ -18,12 +25,17 @@ const mapStateToProps = state => ({ rulesLastUpdated: getRulesLastUpdated(state), deviceGroups: getDeviceGroups(state), theme: getTheme(state), - timeSeriesExplorerUrl: getTimeSeriesExplorerUrl(state) + timeSeriesExplorerUrl: getTimeSeriesExplorerUrl(state), + deviceModuleStatus: getDeviceModuleStatus(state), + isDeviceModuleStatusPending: getDeviceModuleStatusPendingStatus(state), + deviceModuleStatusError: getDeviceModuleStatusError(state) }); // Wrap the dispatch method const mapDispatchToProps = dispatch => ({ fetchRules: () => dispatch(ruleEpics.actions.fetchRules()), + fetchModules: (deviceId) => dispatch(devicesEpics.actions.fetchEdgeAgent(deviceId)), + resetPendingAndError: () => dispatch(devicesRedux.actions.resetPendingAndError(devicesEpics.actions.fetchEdgeAgent)) }); export const DeviceDetailsContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeviceDetails)); diff --git a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js index 1b7bd344e..110aa1d13 100644 --- a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js +++ b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.js @@ -32,6 +32,7 @@ import { import Flyout from 'components/shared/flyout'; import { TelemetryChart, chartColorObjects } from 'components/pages/dashboard/panels/telemetry'; import { transformTelemetryResponse } from 'components/pages/dashboard/panels'; +import { getEdgeAgentStatusCode } from 'utilities'; import './deviceDetails.css'; @@ -50,9 +51,10 @@ export class DeviceDetails extends Component { telemetryIsPending: true, telemetryError: null, - showRawMessage: false + showRawMessage: false, + currentModuleStatus: undefined }; - + this.baseState = this.state; this.columnDefs = [ { ...rulesColumnDefs.ruleName, @@ -65,6 +67,14 @@ export class DeviceDetails extends Component { this.resetTelemetry$ = new Subject(); this.telemetryRefresh$ = new Subject(); + if (this.props.moduleStatus) { + this.state = { + ...this.state, + currentModuleStatus: this.props.moduleStatus + }; + } else { + this.props.fetchModules(this.props.device.id); + } } componentDidMount() { @@ -119,11 +129,49 @@ export class DeviceDetails extends Component { } componentWillReceiveProps(nextProps) { - if ((this.props.device || {}).id !== nextProps.device.id) { - const deviceId = (nextProps.device || {}).id; + const { + deviceModuleStatus, + isDeviceModuleStatusPending, + deviceModuleStatusError, + moduleStatus, + resetPendingAndError, + device, + fetchModules + } = nextProps; + let tempState = {}; + /* + deviceModuleStatus is a prop fetched by making fetchModules() API call through deviceDetails.container on demand. + moduleStatus is a prop sent from deploymentDetailsGrid which it already has in rowData. + Both deviceModuleStatus and moduleStatus have the same content, + but come from different sources based on the page that opens this flyout. + Depending on which one is available, currentModuleStatus is set in component state. + */ + + if ((this.props.device || {}).id !== device.id) { + // Reset state if the device changes. + resetPendingAndError(); + tempState = { ...this.baseState }; + + if (moduleStatus) { + // If moduleStatus exist in props, set it in state. + tempState = { + ...tempState, + currentModuleStatus: moduleStatus + }; + } else { + // Otherwise make an API call to get deviceModuleStatus. + fetchModules(device.id); + } + + const deviceId = (device || {}).id; this.resetTelemetry$.next(deviceId); this.fetchAlerts(deviceId); + } else if (!moduleStatus && !isDeviceModuleStatusPending && !deviceModuleStatusError) { + // set deviceModuleStatus in state, if moduleStatus doesn't exist and devicesReducer successfully received the API response. + tempState = { currentModuleStatus: deviceModuleStatus }; } + + if (Object.keys(tempState).length) this.setState(tempState); } componentWillUnmount() { @@ -162,8 +210,16 @@ export class DeviceDetails extends Component { } render() { - const { t, onClose, device, theme, timeSeriesExplorerUrl } = this.props; - const { telemetry, lastMessage } = this.state; + const { + t, + onClose, + device, + theme, + timeSeriesExplorerUrl, + isDeviceModuleStatusPending, + deviceModuleStatusError + } = this.props; + const { telemetry, lastMessage, currentModuleStatus } = this.state; const lastMessageTime = (lastMessage || {}).time; const isPending = this.state.isAlertsPending && this.props.isRulesPending; const rulesGridProps = { @@ -176,8 +232,12 @@ export class DeviceDetails extends Component { }; const tags = Object.entries(device.tags || {}); const properties = Object.entries(device.properties || {}); - + const moduleQuerySuccessful = currentModuleStatus && + currentModuleStatus !== {} && + !isDeviceModuleStatusPending && + !deviceModuleStatusError; // Add parameters to Time Series Insights Url + const timeSeriesParamUrl = timeSeriesExplorerUrl ? timeSeriesExplorerUrl + @@ -391,6 +451,28 @@ export class DeviceDetails extends Component { + + + {t('devices.flyouts.details.modules.title')} + + + {t("devices.flyouts.details.modules.description")} + +
+ { + !moduleQuerySuccessful && + t('devices.flyouts.details.modules.noneExist') + } + { + moduleQuerySuccessful && + +
{currentModuleStatus.code}: {getEdgeAgentStatusCode(currentModuleStatus.code, t)}
+
{currentModuleStatus.description}
+
+ } +
+
+
} diff --git a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.scss b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.scss index 20b4eb214..a9fafed83 100644 --- a/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.scss +++ b/src/components/pages/devices/flyouts/deviceDetails/deviceDetails.scss @@ -27,6 +27,13 @@ } } + .device-details-deployment-contentbox { + @include rem-fallback(padding, 20px, 0px, 20px, 20px); + @include rem-fallback(border-top, 1px, solid); + @include rem-fallback(margin-top, 20px); + @include rem-fallback(border-bottom, 1px, solid); + } + .device-properties-actions .row { border: 0; @include rem-fallback(font-size, 12px); @@ -53,5 +60,7 @@ } .device-properties-actions .row { color: themed('colorContentTextDim'); } + + .device-details-deployment-contentbox { border-color: themed('colorHeaderBorderColor'); } } } diff --git a/src/services/iotHubManagerService.js b/src/services/iotHubManagerService.js index b51d62662..49b855226 100644 --- a/src/services/iotHubManagerService.js +++ b/src/services/iotHubManagerService.js @@ -82,7 +82,7 @@ export class IoTHubManagerService { } /** Queries EdgeAgent */ - static getEdgeAgentsByQuery(query) { + static getModulesByQuery(query) { return HttpClient.post(`${ENDPOINT}modules/query`, query) .map(toEdgeAgentsModel); } diff --git a/src/services/models/iotHubManagerModels.js b/src/services/models/iotHubManagerModels.js index 1ca5d3f8e..6a04cf3d5 100644 --- a/src/services/models/iotHubManagerModels.js +++ b/src/services/models/iotHubManagerModels.js @@ -210,7 +210,8 @@ export const toDeploymentRequestModel = (deploymentModel = {}) => ({ export const toEdgeAgentModel = (edgeAgent = {}) => camelCaseReshape(edgeAgent, { 'deviceId': 'id', - 'reported.lastDesiredStatus.code': 'lastMessage', + 'reported.lastDesiredStatus.description': 'description', + 'reported.lastDesiredStatus.code': 'code', 'reported.systemModules.edgeAgent.lastStartTimeUtc': 'start', 'reported.systemModules.edgeAgent.lastExitTimeUtc': 'end' }); diff --git a/src/store/reducers/deploymentsReducer.js b/src/store/reducers/deploymentsReducer.js index 7af4d1f3a..abed19818 100644 --- a/src/store/reducers/deploymentsReducer.js +++ b/src/store/reducers/deploymentsReducer.js @@ -58,7 +58,7 @@ export const epics = createEpicScenario({ type: 'DEPLOYED_DEVICES_FETCH', epic: fromAction => Observable .forkJoin( - IoTHubManagerService.getEdgeAgentsByQuery(createEdgeAgentQuery(getDeployedDeviceIds(fromAction.payload))), + IoTHubManagerService.getModulesByQuery(createEdgeAgentQuery(getDeployedDeviceIds(fromAction.payload))), IoTHubManagerService.getDevicesByQuery(createDevicesQuery(getDeployedDeviceIds(fromAction.payload))), ) .map(toActionCreator(redux.actions.updateDeployedDevices, fromAction)) @@ -161,6 +161,13 @@ const updateDeployedDevicesReducer = (state, { payload: [modules, devices], from }); }; +const resetDeployedDevicesReducer = (state) => update(state, { + entities: { + $unset: ['deployedDevices'] + } +}); + + /* Action types that cause a pending flag */ const fetchableTypes = [ epics.actionTypes.fetchDeployment, @@ -177,6 +184,7 @@ export const redux = createReducerScenario({ updateDeployment: { type: 'DEPLOYMENTS_DETAILS_UPDATE', reducer: updateDeploymentReducer }, updateDeployedDevices: { type: 'DEPLOYED_DEVICES_UPDATE', reducer: updateDeployedDevicesReducer }, registerError: { type: 'DEPLOYMENTS_REDUCER_ERROR', reducer: errorReducer }, + resetDeployedDevices: { type: 'DEPLOYMETS_RESET_DEPLOYED_DEVICES', reducer: resetDeployedDevicesReducer }, resetPendingAndError: { type: 'DEPLOYMENTS_REDUCER_RESET_ERROR_PENDING', reducer: resetPendingAndErrorReducer }, isFetching: { multiType: fetchableTypes, reducer: pendingReducer } }); diff --git a/src/store/reducers/devicesReducer.js b/src/store/reducers/devicesReducer.js index 0be6619e1..d4081f6fd 100644 --- a/src/store/reducers/devicesReducer.js +++ b/src/store/reducers/devicesReducer.js @@ -47,6 +47,17 @@ export const epics = createEpicScenario({ } }, + /** Loads EdgeAgent json from device modules */ + fetchEdgeAgent: { + type: 'DEVICES_FETCH_EDGE_AGENT', + epic: fromAction => IoTHubManagerService + .getModulesByQuery(`"deviceId IN ['${fromAction.payload}'] AND moduleId = '$edgeAgent'"`) + .map(([edgeAgent]) => edgeAgent) + .map(toActionCreator(redux.actions.updateModuleStatus, fromAction)) + .catch(handleError(fromAction)) + + }, + /* Update the devices if the selected device group changes */ refreshDevices: { type: 'DEVICES_REFRESH', @@ -124,6 +135,17 @@ const updateTagsReducer = (state, { payload }) => { }); }; +const updateModuleStatusReducer = (state, { payload, fromAction }) => { + const updateAction = payload + ? { deviceModuleStatus: { $set: payload } } + : { $unset: ['deviceModuleStatus'] }; + + return update(state, { + ...updateAction, + ...setPending(fromAction.type, false) + }); +}; + const updatePropertiesReducer = (state, { payload }) => { const updatedPropertyData = {}; payload.updatedProperties.forEach(({ name, value }) => (updatedPropertyData[name] = value)); @@ -145,7 +167,8 @@ const updatePropertiesReducer = (state, { payload }) => { /* Action types that cause a pending flag */ const fetchableTypes = [ epics.actionTypes.fetchDevices, - epics.actionTypes.fetchDevicesByCondition + epics.actionTypes.fetchDevicesByCondition, + epics.actionTypes.fetchEdgeAgent ]; export const redux = createReducerScenario({ @@ -156,6 +179,7 @@ export const redux = createReducerScenario({ insertDevices: { type: 'DEVICE_INSERT', reducer: insertDevicesReducer }, updateTags: { type: 'DEVICE_UPDATE_TAGS', reducer: updateTagsReducer }, updateProperties: { type: 'DEVICE_UPDATE_PROPERTIES', reducer: updatePropertiesReducer }, + updateModuleStatus: { type: 'DEVICE_MODULE_STATUS', reducer: updateModuleStatusReducer }, resetPendingAndError: { type: 'DEVICE_REDUCER_RESET_ERROR_PENDING', reducer: resetPendingAndErrorReducer } }); @@ -179,4 +203,17 @@ export const getDevices = createSelector( getEntities, getItems, (entities, items) => items.map(id => entities[id]) ); +export const getDeviceModuleStatus = state => { + const deviceModuleStatus = getDevicesReducer(state).deviceModuleStatus + return deviceModuleStatus + ? { + code: deviceModuleStatus.code, + description: deviceModuleStatus.description + } + : undefined +}; +export const getDeviceModuleStatusPendingStatus = state => + getPending(getDevicesReducer(state), epics.actionTypes.fetchEdgeAgent); +export const getDeviceModuleStatusError = state => + getError(getDevicesReducer(state), epics.actionTypes.fetchEdgeAgent); // ========================= Selectors - END diff --git a/src/utilities/methods.js b/src/utilities/methods.js index f252e516e..6c8a4498a 100644 --- a/src/utilities/methods.js +++ b/src/utilities/methods.js @@ -99,6 +99,19 @@ export const getStatusCode = (code, t) => { } } +/** Converts a deployment status code to a translated string equivalent */ +export const getEdgeAgentStatusCode = (code, t) => { + switch (code) { + case 200: return t('edgeAgentStatus.200'); + case 400: return t('edgeAgentStatus.400'); + case 406: return t('edgeAgentStatus.406'); + case 412: return t('edgeAgentStatus.412'); + case 417: return t('edgeAgentStatus.417'); + case 500: return t('edgeAgentStatus.500'); + default: return t('edgeAgentStatus.unknown'); + } +} + /* A helper method to copy text to the clipbaord */ export const copyToClipboard = (data) => { const textField = document.createElement('textarea'); From d1168166760416d2bd2e022e170cc422abdb6819 Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 5 Oct 2018 17:20:53 -0700 Subject: [PATCH 22/25] =?UTF-8?q?Replacing=20ids=20with=20Names=20and=20re?= =?UTF-8?q?moving=20IOThubmanager's=20dependency=20on=20c=E2=80=A6=20(#112?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * replacing ids with Names and removing IOThubmanager's dependency on config service * type --- public/locales/en/translations.json | 3 +- .../deploymentDetails/deploymentDetails.js | 12 +++---- .../deploymentsGrid/deploymentsGridConfig.js | 7 ++-- .../flyouts/deploymentNew/deploymentNew.js | 33 +++++++++++++++---- src/services/models/configModels.js | 3 +- src/services/models/iotHubManagerModels.js | 8 ++++- src/utilities/methods.js | 8 +++++ 7 files changed, 56 insertions(+), 18 deletions(-) diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index a6c54905b..63ba2bfc7 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -617,7 +617,8 @@ } }, "typeOptions": { - "edgemanifest": "Edge Manifest" + "edgemanifest": "Edge Manifest", + "unknown": "Unknown" }, "grid": { "name": "Name", diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js index f6a4cb127..06ae37969 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js @@ -18,7 +18,7 @@ import { StatProperty } from 'components/shared'; import { TimeRenderer } from 'components/shared/cellRenderers'; -import { svgs } from 'utilities'; +import { getPackageTypeTranslation, svgs } from 'utilities'; import { DeploymentDetailsGrid } from './deploymentDetailsGrid/deploymentDetailsGrid'; import "./deploymentDetails.css"; @@ -90,10 +90,10 @@ export class DeploymentDetails extends Component { failedCount, name, priority, - deviceGroupId, + deviceGroupName, createdDateTimeUtc, type, - packageId + packageName } = currentDeployment; const pendingCalc = appliedCount - succeededCount - failedCount; const pendingCount = pendingCalc ? pendingCalc : '0'; @@ -164,7 +164,7 @@ export class DeploymentDetails extends Component { {t('deployments.details.deviceGroup')}
- {deviceGroupId} + {deviceGroupName}
@@ -182,7 +182,7 @@ export class DeploymentDetails extends Component { {t('deployments.details.packageType')}
- {type} + {type ? getPackageTypeTranslation(type, t) : undefined}
@@ -190,7 +190,7 @@ export class DeploymentDetails extends Component { {t('deployments.details.package')}
- {packageId} + {packageName}
diff --git a/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js b/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js index 9f0fc86f9..fa9133e4a 100644 --- a/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js +++ b/src/components/pages/deployments/deploymentsHome/deploymentsGrid/deploymentsGridConfig.js @@ -2,6 +2,7 @@ import Config from 'app.config'; import { SoftSelectLinkRenderer, TimeRenderer } from 'components/shared/cellRenderers'; +import { getPackageTypeTranslation } from 'utilities'; import { gridValueFormatters } from 'components/shared/pcsGrid/pcsGridConfig'; const { checkForEmpty } = gridValueFormatters; @@ -16,12 +17,12 @@ export const deploymentsColumnDefs = { }, package: { headerName: 'deployments.grid.package', - field: 'packageId', + field: 'packageName', valueFormatter: ({ value }) => checkForEmpty(value) }, deviceGroup: { headerName: 'deployments.grid.deviceGroup', - field: 'deviceGroupId', + field: 'deviceGroupName', valueFormatter: ({ value }) => checkForEmpty(value) }, priority: { @@ -32,7 +33,7 @@ export const deploymentsColumnDefs = { type: { headerName: 'deployments.grid.type', field: 'type', - valueFormatter: ({ value }) => checkForEmpty(value) + valueFormatter: ({ value, context: { t } }) => getPackageTypeTranslation(checkForEmpty(value), t) }, applied: { headerName: 'deployments.grid.applied', diff --git a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js index 5c2337a22..91cd898a6 100644 --- a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js +++ b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js @@ -35,6 +35,7 @@ export class DeploymentNew extends LinkedComponent { packageType: undefined, deviceGroupId: undefined, deviceGroupName: '', + deviceGroupQuery: '', name: '', priority: '', packageId: undefined, @@ -70,10 +71,29 @@ export class DeploymentNew extends LinkedComponent { apply = (event) => { event.preventDefault(); - const { createDeployment } = this.props; - const { packageType, deviceGroupId, name, priority, packageId } = this.state; + const { createDeployment, packages } = this.props; + const { + packageName, + deviceGroupName, + deviceGroupQuery, + deviceGroupId, + name, + priority, + packageId, + packageType } = this.state; if (this.formIsValid()) { - createDeployment({ 'type': packageType, deviceGroupId, name, priority, packageId }); + const packageContent = packages.find(packageObj => packageObj.id === packageId).content; + createDeployment({ + type: packageType, + packageName, + packageContent, + packageId, + deviceGroupName, + deviceGroupQuery, + deviceGroupId, + name, + priority + }); this.setState({ changesApplied: true }); } } @@ -88,7 +108,7 @@ export class DeploymentNew extends LinkedComponent { ].every(link => !link.error); } - onPackageSelected = (e) => { + onPackageTypeSelected = (e) => { switch (e.target.value.value) { // case Edge manifest case 'EdgeManifest': @@ -106,7 +126,8 @@ export class DeploymentNew extends LinkedComponent { onDeviceGroupSelected = (e) => { const { fetchDevices, deviceGroups } = this.props; const selectedDeviceGroupId = e.target.value.value; - const selectedDeviceGroup = deviceGroups.find(deviceGroup => deviceGroup.id === selectedDeviceGroupId) + const selectedDeviceGroup = deviceGroups.find(deviceGroup => deviceGroup.id === selectedDeviceGroupId); + this.setState({ deviceGroupQuery: JSON.stringify(selectedDeviceGroup.conditions) }); fetchDevices(selectedDeviceGroup.conditions); } @@ -199,7 +220,7 @@ export class DeploymentNew extends LinkedComponent { type="select" className="long" link={this.packageTypeLink} - onChange={this.onPackageSelected} + onChange={this.onPackageTypeSelected} options={typeOptions} placeholder={t('deployments.flyouts.new.typePlaceHolder')} clearable={false} diff --git a/src/services/models/configModels.js b/src/services/models/configModels.js index 26dcaf14c..a178033b7 100644 --- a/src/services/models/configModels.js +++ b/src/services/models/configModels.js @@ -74,6 +74,7 @@ export const toPackageModel = (response = {}) => { 'id': 'id', 'type': 'type', 'name': 'name', - 'dateCreated': 'dateCreated' + 'dateCreated': 'dateCreated', + 'content': 'content' }); }; diff --git a/src/services/models/iotHubManagerModels.js b/src/services/models/iotHubManagerModels.js index 6a04cf3d5..2d0f950d7 100644 --- a/src/services/models/iotHubManagerModels.js +++ b/src/services/models/iotHubManagerModels.js @@ -183,7 +183,9 @@ export const toDeploymentModel = (deployment = {}) => { 'id': 'id', 'name': 'name', 'deviceGroupId': 'deviceGroupId', - 'packageId': 'packageId', + 'deviceGroupQuery': 'deviceGroupQuery', + 'deviceGroupName': 'deviceGroupName', + 'packageName': 'packageName', 'priority': 'priority', 'type': 'type', 'createdDateTimeUtc': 'createdDateTimeUtc', @@ -202,8 +204,12 @@ export const toDeploymentsModel = (response = {}) => getItems(response) export const toDeploymentRequestModel = (deploymentModel = {}) => ({ DeviceGroupId: deploymentModel.deviceGroupId, + DeviceGroupName: deploymentModel.deviceGroupName, + DeviceGroupQuery: deploymentModel.deviceGroupQuery, Name: deploymentModel.name, PackageId: deploymentModel.packageId, + PackageName: deploymentModel.packageName, + PackageContent: deploymentModel.packageContent, Priority: deploymentModel.priority, Type: deploymentModel.type }); diff --git a/src/utilities/methods.js b/src/utilities/methods.js index 6c8a4498a..ef43311e6 100644 --- a/src/utilities/methods.js +++ b/src/utilities/methods.js @@ -112,6 +112,14 @@ export const getEdgeAgentStatusCode = (code, t) => { } } +/** Converts a packageType enum to a translated string equivalent */ +export const getPackageTypeTranslation = (packageType, t) => { + switch (packageType.toLowerCase()) { + case 'edgemanifest': return t('deployments.typeOptions.edgemanifest'); + default: return t('deployments.typeOptions.unknown'); + } +} + /* A helper method to copy text to the clipbaord */ export const copyToClipboard = (data) => { const textField = document.createElement('textarea'); From b2f55ef02bd08a988caf078c01b2b1bd1a19b2fb Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 8 Oct 2018 09:53:48 -0700 Subject: [PATCH 23/25] Adding In-porduct telemetry for Packages and Deployments (#1122) * Adding In-porduct telemetry for Packages and Deployments * remove unused model --- .../deploymentDetails.container.js | 8 ++- .../deploymentDetails/deploymentDetails.js | 18 ++++-- .../deploymentDetailsGrid.js | 8 ++- .../deploymentsHome/deployments.container.js | 8 ++- .../deploymentsHome/deployments.js | 20 +++++-- .../deploymentNew/deploymentNew.container.js | 8 ++- .../flyouts/deploymentNew/deploymentNew.js | 55 ++++++++++++++++--- .../packageNew/packageNew.container.js | 2 + .../packages/flyouts/packageNew/packageNew.js | 42 ++++++++++++-- .../deletePackage/deletePackage.container.js | 4 +- .../pages/packages/packages.container.js | 8 ++- src/components/pages/packages/packages.js | 18 ++++-- .../shared/deleteModal/deleteModal.js | 27 +++++++-- src/services/models/logEventModels.js | 10 ++-- 14 files changed, 189 insertions(+), 47 deletions(-) diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js index 6e49272ad..f9192047b 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.container.js @@ -16,6 +16,10 @@ import { epics as deploymentsEpics, redux as deploymentsRedux } from 'store/reducers/deploymentsReducer'; +import { + redux as appRedux, + epics as appEpics, +} from 'store/reducers/appReducer'; // Pass the global info needed const mapStateToProps = state => ({ @@ -34,7 +38,9 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ fetchDeployment: id => dispatch(deploymentsEpics.actions.fetchDeployment(id)), resetDeployedDevices: () => dispatch(deploymentsRedux.actions.resetDeployedDevices()), - deleteItem: deploymentId => dispatch(deploymentsEpics.actions.deleteDeployment(deploymentId)) + deleteItem: deploymentId => dispatch(deploymentsEpics.actions.deleteDeployment(deploymentId)), + updateCurrentWindow: (currentWindow) => dispatch(appRedux.actions.updateCurrentWindow(currentWindow)), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const DeploymentDetailsContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeploymentDetails)); diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js index 06ae37969..79bccca1d 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import React, { Component } from 'react'; -import { permissions } from 'services/models'; +import { permissions, toSinglePropertyDiagnosticsModel } from 'services/models'; import { AjaxError, Btn, @@ -35,6 +35,9 @@ export class DeploymentDetails extends Component { ...closedModalState, deploymentDeleted: false }; + + this.props.updateCurrentWindow('DeploymentDetails'); + props.fetchDeployment(props.match.params.id); } @@ -43,8 +46,13 @@ export class DeploymentDetails extends Component { } getOpenModal = () => { - const { t, deleteIsPending, deleteError, deleteItem } = this.props; + const { t, deleteIsPending, deleteError, deleteItem, logEvent } = this.props; if (this.state.openModalName === 'delete-deployment' && this.props.currentDeployment) { + logEvent( + toSinglePropertyDiagnosticsModel( + 'DeploymentDetail_DeleteClick', + 'DeploymentId', + this.props.currentDeployment ? this.props.currentDeployment.id : '')); return } - {!isDeployedDevicesPending && } + {!isDeployedDevicesPending && }
); diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js index 3d3ced3d8..2de665925 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetailsGrid/deploymentDetailsGrid.js @@ -5,6 +5,7 @@ import { deploymentDetailsColumnDefs, defaultDeploymentDetailsGridProps } from ' import { translateColumnDefs, isFunc } from 'utilities'; import { PcsGrid, ComponentArray } from 'components/shared'; import { DeviceDetailsContainer } from 'components/pages/devices/flyouts'; +import { toSinglePropertyDiagnosticsModel } from 'services/models'; const closedFlyoutState = { openFlyoutName: undefined, @@ -43,8 +44,13 @@ export class DeploymentDetailsGrid extends Component { }); onSoftSelectChange = (deviceRowId, rowEvent) => { - const { onSoftSelectChange } = this.props; + const { onSoftSelectChange, logEvent } = this.props; const rowData = (this.deployedDevicesGridApi.getDisplayedRowAtIndex(deviceRowId) || {}).data; + logEvent( + toSinglePropertyDiagnosticsModel( + 'DeploymentDetail_DeviceGridClick', + 'DeviceId', + rowData ? rowData.id : '')); if (rowData && rowData.device) { this.setState({ openFlyoutName: 'deviceDetails', diff --git a/src/components/pages/deployments/deploymentsHome/deployments.container.js b/src/components/pages/deployments/deploymentsHome/deployments.container.js index f8f878974..5a7277e3f 100644 --- a/src/components/pages/deployments/deploymentsHome/deployments.container.js +++ b/src/components/pages/deployments/deploymentsHome/deployments.container.js @@ -10,6 +10,10 @@ import { getDeploymentsLastUpdated, epics as deploymentsEpics } from 'store/reducers/deploymentsReducer'; +import { + redux as appRedux, + epics as appEpics, +} from 'store/reducers/appReducer'; // Pass the global info needed const mapStateToProps = state => ({ @@ -21,7 +25,9 @@ const mapStateToProps = state => ({ // Wrap the dispatch methods const mapDispatchToProps = dispatch => ({ - fetchDeployments: () => dispatch(deploymentsEpics.actions.fetchDeployments()) + fetchDeployments: () => dispatch(deploymentsEpics.actions.fetchDeployments()), + updateCurrentWindow: (currentWindow) => dispatch(appRedux.actions.updateCurrentWindow(currentWindow)), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const DeploymentsContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Deployments)); diff --git a/src/components/pages/deployments/deploymentsHome/deployments.js b/src/components/pages/deployments/deploymentsHome/deployments.js index e3d27ddf9..4fa7f7341 100644 --- a/src/components/pages/deployments/deploymentsHome/deployments.js +++ b/src/components/pages/deployments/deploymentsHome/deployments.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; -import { permissions } from 'services/models'; +import { permissions, toDiagnosticsModel } from 'services/models'; import { AjaxError, Btn, @@ -32,6 +32,8 @@ export class Deployments extends Component { contextBtns: null }; + this.props.updateCurrentWindow('Deployments'); + if (!this.props.lastUpdated && !this.props.error) { this.props.fetchDeployments(); } @@ -51,16 +53,24 @@ export class Deployments extends Component { openFlyoutName: undefined }); - openNewDeploymentFlyout = () => this.setState({ - openFlyoutName: 'newDeployment' - }); - + openNewDeploymentFlyout = () => { + this.props.logEvent(toDiagnosticsModel('Deployments_NewClick', {})); + this.setState({ + openFlyoutName: 'newDeployment' + }); + } onGridReady = gridReadyEvent => this.deploymentGridApi = gridReadyEvent.api; getSoftSelectId = ({ id } = '') => id; onSoftSelectChange = (deviceRowId) => { const rowData = (this.deploymentGridApi.getDisplayedRowAtIndex(deviceRowId) || {}).data; + this.props.logEvent( + toDiagnosticsModel('Deployments_GridRowClick', { + id: rowData.id, + displayName: rowData.name + }) + ); this.props.history.push(`/deployments/${rowData.id}`) } diff --git a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js index 01e0838ee..16f61a0ad 100644 --- a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js +++ b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js @@ -16,7 +16,10 @@ import { epics as packagesEpics, redux as packagesRedux } from 'store/reducers/packagesReducer'; -import { getDeviceGroups } from 'store/reducers/appReducer'; +import { + getDeviceGroups, + epics as appEpics, +} from 'store/reducers/appReducer'; import { getDevices, getDevicesByConditionError, @@ -45,7 +48,8 @@ const mapDispatchToProps = dispatch => ({ fetchPackages: () => dispatch(packagesEpics.actions.fetchPackages()), resetPackagesPendingError: () => dispatch(packagesRedux.actions.resetPendingAndError(packagesEpics.actions.fetchPackages)), fetchDevices: condition => dispatch(devicesEpics.actions.fetchDevicesByCondition(condition)), - resetDevicesPendingError: () => dispatch(devicesRedux.actions.resetPendingAndError(devicesEpics.actions.fetchDevicesByCondition)) + resetDevicesPendingError: () => dispatch(devicesRedux.actions.resetPendingAndError(devicesEpics.actions.fetchDevicesByCondition)), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const DeploymentNewContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeploymentNew)); diff --git a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js index 91cd898a6..e3da27cef 100644 --- a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js +++ b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js @@ -2,7 +2,7 @@ import React from 'react'; -import { packageTypeOptions } from 'services/models'; +import { packageTypeOptions, toDiagnosticsModel, toSinglePropertyDiagnosticsModel } from 'services/models'; import { svgs, LinkedComponent, Validator } from 'utilities'; import { AjaxError, @@ -71,7 +71,7 @@ export class DeploymentNew extends LinkedComponent { apply = (event) => { event.preventDefault(); - const { createDeployment, packages } = this.props; + const { createDeployment, packages, logEvent } = this.props; const { packageName, deviceGroupName, @@ -81,6 +81,18 @@ export class DeploymentNew extends LinkedComponent { priority, packageId, packageType } = this.state; + + logEvent( + toDiagnosticsModel( + 'NewDeployment_ApplyClick', { + packageId, + packageType, + priority, + name, + deviceGroupId, + packageName, + }) + ); if (this.formIsValid()) { const packageContent = packages.find(packageObj => packageObj.id === packageId).content; createDeployment({ @@ -109,7 +121,12 @@ export class DeploymentNew extends LinkedComponent { } onPackageTypeSelected = (e) => { - switch (e.target.value.value) { + const selectedPackageType = e.target.value.value; + this.props.logEvent( + toSinglePropertyDiagnosticsModel('NewDeployment_PackageTypeSelect', 'PackageType', selectedPackageType) + ); + + switch (selectedPackageType) { // case Edge manifest case 'EdgeManifest': const { fetchPackages } = this.props; @@ -126,6 +143,9 @@ export class DeploymentNew extends LinkedComponent { onDeviceGroupSelected = (e) => { const { fetchDevices, deviceGroups } = this.props; const selectedDeviceGroupId = e.target.value.value; + this.props.logEvent( + toSinglePropertyDiagnosticsModel('NewDeployment_DeviceGroupSelect', 'DeviceGroup', selectedDeviceGroupId) + ); const selectedDeviceGroup = deviceGroups.find(deviceGroup => deviceGroup.id === selectedDeviceGroupId); this.setState({ deviceGroupQuery: JSON.stringify(selectedDeviceGroup.conditions) }); fetchDevices(selectedDeviceGroup.conditions); @@ -141,10 +161,22 @@ export class DeploymentNew extends LinkedComponent { toDeviceGroupSelectOption = ({ id, displayName }) => ({ label: displayName, value: id }); + genericCloseClick = (eventName) => { + const { onClose, logEvent } = this.props; + logEvent(toDiagnosticsModel(eventName, {})); + onClose(); + } + + genericOnChange = (eventName, key, value) => { + this.props.logEvent( + toSinglePropertyDiagnosticsModel(eventName, key, value) + ); + this.formControlChange(); + } + render() { const { t, - onClose, createIsPending, createError, packagesPending, @@ -193,7 +225,7 @@ export class DeploymentNew extends LinkedComponent { {t('deployments.flyouts.new.title')} - + this.genericCloseClick('NewDeployment_CloseClick')} />
@@ -205,7 +237,7 @@ export class DeploymentNew extends LinkedComponent { type="text" className="long" link={this.nameLink} - onChange={this.formControlChange} + onChange={(target) => this.genericOnChange('NewDeployment_NameText', 'Name', target.value)} placeholder={t('deployments.flyouts.new.namePlaceHolder')} /> } { @@ -239,6 +271,7 @@ export class DeploymentNew extends LinkedComponent { disabled={!isPackageTypeSelected} link={this.packageIdLink} options={packageOptions} + onChange={(target) => this.genericOnChange('NewDeployment_PackageSelect', 'Package', target.value.value)} placeholder={isPackageTypeSelected ? t('deployments.flyouts.new.packagePlaceHolder') : ""} clearable={false} searchable={false} /> @@ -280,7 +313,7 @@ export class DeploymentNew extends LinkedComponent { type="text" className="long" link={this.priorityLink} - onChange={this.formControlChange} + onChange={(target) => this.genericOnChange('NewDeployment_PriorityNumber', 'Priority', target.value)} placeholder={t('deployments.flyouts.new.priorityPlaceHolder')} /> } { @@ -326,14 +359,18 @@ export class DeploymentNew extends LinkedComponent { (!completedSuccessfully) && {t('deployments.flyouts.new.apply')} - {t('deployments.flyouts.new.cancel')} + this.genericCloseClick('NewDeployment_CancelClick')}>{ + t('deployments.flyouts.new.cancel')} + } { /** After successful deployment creation, show only close button. */ (completedSuccessfully) && - {t('deployments.flyouts.new.close')} + this.genericCloseClick('NewDeployment_CancelClick')}> + {t('deployments.flyouts.new.close')} + } diff --git a/src/components/pages/packages/flyouts/packageNew/packageNew.container.js b/src/components/pages/packages/flyouts/packageNew/packageNew.container.js index ef493cf44..95e9f2ea1 100644 --- a/src/components/pages/packages/flyouts/packageNew/packageNew.container.js +++ b/src/components/pages/packages/flyouts/packageNew/packageNew.container.js @@ -9,6 +9,7 @@ import { epics as packagesEpics, redux as packagesRedux } from 'store/reducers/packagesReducer'; +import { epics as appEpics } from 'store/reducers/appReducer'; // Pass the global info needed const mapStateToProps = state => ({ @@ -20,6 +21,7 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ createPackage: packageModel => dispatch(packagesEpics.actions.createPackage(packageModel)), resetPackagesPendingError: () => dispatch(packagesRedux.actions.resetPendingAndError(packagesEpics.actions.createPackage)), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const PackageNewContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(PackageNew)); diff --git a/src/components/pages/packages/flyouts/packageNew/packageNew.js b/src/components/pages/packages/flyouts/packageNew/packageNew.js index a7eb5a271..3582f44e7 100644 --- a/src/components/pages/packages/flyouts/packageNew/packageNew.js +++ b/src/components/pages/packages/flyouts/packageNew/packageNew.js @@ -2,7 +2,11 @@ import React from 'react'; -import { packageTypeOptions } from 'services/models'; +import { + packageTypeOptions, + toSinglePropertyDiagnosticsModel, + toDiagnosticsModel +} from 'services/models'; import { svgs, LinkedComponent, Validator } from 'utilities'; import { AjaxError, @@ -47,15 +51,28 @@ export class PackageNew extends LinkedComponent { event.preventDefault(); const { createPackage } = this.props; const { type, packageFile } = this.state; + this.props.logEvent( + toDiagnosticsModel( + 'NewPackage_Apply', + { + type, + packageName: packageFile.name + }) + ); if (this.formIsValid()) { createPackage({ type: type, packageFile: packageFile }); this.setState({ changesApplied: true }); } } + packageTypeChange = ({ target: { value: { value = {} } } }) => { + this.props.logEvent(toSinglePropertyDiagnosticsModel('NewPackage_TypeClick', 'Type', value)); + } + onFileSelected = (e) => { let file = e.target.files[0]; this.setState({ packageFile: file }); + this.props.logEvent(toSinglePropertyDiagnosticsModel('NewPackage_FileSelect', 'FileName', file.name)); } formIsValid = () => { @@ -64,8 +81,14 @@ export class PackageNew extends LinkedComponent { ].every(link => !link.error); } + genericCloseClick = (eventName) => { + const { onClose, logEvent } = this.props; + logEvent(toDiagnosticsModel(eventName, {})); + onClose(); + } + render() { - const { t, onClose, isPending, error } = this.props; + const { t, isPending, error } = this.props; const { packageFile, changesApplied } = this.state; const summaryCount = 1; @@ -85,7 +108,7 @@ export class PackageNew extends LinkedComponent { {t('packages.flyouts.new.title')} - + this.genericCloseClick('NewPackage_CloseClick')} /> @@ -97,6 +120,7 @@ export class PackageNew extends LinkedComponent { {t('packages.flyouts.new.upload')} - {t('packages.flyouts.new.cancel')} + this.genericCloseClick('NewPackage_CancelClick')}> + {t('packages.flyouts.new.cancel')} + } { /** If package is not selected, show only the cancel button. */ (!packageFile) && - {t('packages.flyouts.new.cancel')} + this.genericCloseClick('NewPackage_CancelClick')}> + {t('packages.flyouts.new.cancel')} + } { /** After successful upload, show close button. */ (completedSuccessfully) && - {t('packages.flyouts.new.close')} + this.genericCloseClick('NewPackage_CancelClick')}> + {t('packages.flyouts.new.close')} + } diff --git a/src/components/pages/packages/modals/deletePackage/deletePackage.container.js b/src/components/pages/packages/modals/deletePackage/deletePackage.container.js index a55ce11c8..d17c899f7 100644 --- a/src/components/pages/packages/modals/deletePackage/deletePackage.container.js +++ b/src/components/pages/packages/modals/deletePackage/deletePackage.container.js @@ -8,6 +8,7 @@ import { getDeletePackagePendingStatus, epics as packagesEpics } from 'store/reducers/packagesReducer'; +import { epics as appEpics } from 'store/reducers/appReducer'; // Pass the global info needed const mapStateToProps = state => ({ @@ -17,7 +18,8 @@ const mapStateToProps = state => ({ // Wrap the dispatch methods const mapDispatchToProps = dispatch => ({ - deleteItem: packageId => dispatch(packagesEpics.actions.deletePackage(packageId)) + deleteItem: packageId => dispatch(packagesEpics.actions.deletePackage(packageId)), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const PackageDeleteContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(DeleteModal)); diff --git a/src/components/pages/packages/packages.container.js b/src/components/pages/packages/packages.container.js index 66fe57382..ac62a5499 100644 --- a/src/components/pages/packages/packages.container.js +++ b/src/components/pages/packages/packages.container.js @@ -10,6 +10,10 @@ import { getPackagesLastUpdated, getPackagesPendingStatus } from 'store/reducers/packagesReducer'; +import { + redux as appRedux, + epics as appEpics, +} from 'store/reducers/appReducer'; // Pass the packages status const mapStateToProps = state => ({ @@ -21,7 +25,9 @@ const mapStateToProps = state => ({ // Wrap the dispatch method const mapDispatchToProps = dispatch => ({ - fetchPackages: () => dispatch(packagesEpics.actions.fetchPackages()) + fetchPackages: () => dispatch(packagesEpics.actions.fetchPackages()), + updateCurrentWindow: (currentWindow) => dispatch(appRedux.actions.updateCurrentWindow(currentWindow)), + logEvent: diagnosticsModel => dispatch(appEpics.actions.logEvent(diagnosticsModel)) }); export const PackagesContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(Packages)); diff --git a/src/components/pages/packages/packages.js b/src/components/pages/packages/packages.js index 67b600f44..df90e1c15 100644 --- a/src/components/pages/packages/packages.js +++ b/src/components/pages/packages/packages.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import React, { Component } from 'react'; -import { permissions } from 'services/models'; +import { permissions, toDiagnosticsModel } from 'services/models'; import { PackagesGrid } from './packagesGrid'; import { AjaxError, @@ -29,6 +29,8 @@ export class Packages extends Component { contextBtns: null }; + this.props.updateCurrentWindow('Packages'); + if (!this.props.lastUpdated && !this.props.error) { this.props.fetchPackages(); } @@ -41,16 +43,22 @@ export class Packages extends Component { } } - closeFlyout = () => this.setState(closedFlyoutState); + closeFlyout = () => { + this.props.logEvent(toDiagnosticsModel('Packages_NewClose', {})); + this.setState(closedFlyoutState); + } onContextMenuChange = contextBtns => this.setState({ contextBtns, openFlyoutName: undefined }); - openNewPackageFlyout = () => this.setState({ - openFlyoutName: 'new-Package' - }); + openNewPackageFlyout = () => { + this.props.logEvent(toDiagnosticsModel('Packages_NewClick', {})); + this.setState({ + openFlyoutName: 'new-Package' + }); + } onGridReady = gridReadyEvent => this.packageGridApi = gridReadyEvent.api; diff --git a/src/components/shared/deleteModal/deleteModal.js b/src/components/shared/deleteModal/deleteModal.js index 83d2f73cb..ca6ac1e41 100644 --- a/src/components/shared/deleteModal/deleteModal.js +++ b/src/components/shared/deleteModal/deleteModal.js @@ -10,6 +10,7 @@ import { Modal } from 'components/shared'; import { svgs } from 'utilities'; +import { toSinglePropertyDiagnosticsModel } from 'services/models'; import './deleteModal.css'; @@ -30,20 +31,36 @@ export class DeleteModal extends Component { } apply = () => { - const { deleteItem, itemId } = this.props; + const { deleteItem, itemId, logEvent } = this.props; + logEvent( + toSinglePropertyDiagnosticsModel( + 'DeleteModal_DeleteClick', + 'ItemId', + itemId)); deleteItem(itemId); this.setState({ changesApplied: true }); } + genericCloseClick = (eventName) => { + const { onClose, itemId, logEvent } = this.props; + logEvent( + toSinglePropertyDiagnosticsModel( + eventName, + 'DeleteModal_CloseClick', + 'ItemId', + itemId)); + onClose(); + } + render() { - const { t, onClose, isPending, error, title, deleteInfo } = this.props; + const { t, isPending, error, title, deleteInfo } = this.props; const { changesApplied } = this.state; return ( - + this.genericCloseClick('DeleteModal_ModalClose')} className="delete-modal-container">
{title}
- + this.genericCloseClick('DeleteModal_CloseClick')} svg={svgs.x} />
{deleteInfo} @@ -52,7 +69,7 @@ export class DeleteModal extends Component { { !changesApplied && {t('modal.delete')} - {t('modal.cancel')} + this.genericCloseClick('DeleteModal_CancelClick')}>{t('modal.cancel')} } {isPending && } diff --git a/src/services/models/logEventModels.js b/src/services/models/logEventModels.js index 0bdb511e7..bb70fb1b2 100644 --- a/src/services/models/logEventModels.js +++ b/src/services/models/logEventModels.js @@ -3,11 +3,10 @@ import { toDiagnosticsModel } from 'services/models'; import Config from 'app.config'; -export const toRuleDiagnosticsModel = (eventName, rule) => -{ +export const toRuleDiagnosticsModel = (eventName, rule) => { const metadata = { DeviceGroup: rule.groupId, - Calculation : rule.calculation, + Calculation: rule.calculation, TimePeriod: rule.timePeriod, SeverityLevel: rule.severity, ConditionCount: rule.conditions.length, @@ -23,15 +22,14 @@ export const toSinglePropertyDiagnosticsModel = (eventName, propertyTitle, prope return toDiagnosticsModel(eventName, metadata); } -export const toDeviceDiagnosticsModel = (eventName, deviceFormData) => -{ +export const toDeviceDiagnosticsModel = (eventName, deviceFormData) => { const metadata = { DeviceIDType: deviceFormData.isSimulated ? '' : (deviceFormData.isGenerateId ? 'Generated' : 'Manual'), DeviceType: deviceFormData.isSimulated ? Config.deviceType.simulated : Config.deviceType.physical, NumberOfDevices: deviceFormData.count, DeviceModel: deviceFormData.isSimulated ? deviceFormData.deviceModel : '', AuthType: deviceFormData.isSimulated ? '' : (deviceFormData.authenticationType ? 'x.509' : 'Symmetric Key'), - AuthKey: deviceFormData.isSimulated ? '' : (deviceFormData.isGenerateKeys ? 'Auto': 'Manual') + AuthKey: deviceFormData.isSimulated ? '' : (deviceFormData.isGenerateKeys ? 'Auto' : 'Manual') } return toDiagnosticsModel(eventName, metadata); } From 4678dedaad8af9c986ee7c646ad09b78e9bd92fc Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 8 Oct 2018 23:00:46 -0700 Subject: [PATCH 24/25] Styling update (#1124) * Styling update * red * * new line --- public/locales/en/translations.json | 8 +- .../deploymentDetails/deploymentDetails.js | 84 +++++++------------ .../deploymentDetails/deploymentDetails.scss | 35 ++++++-- .../deploymentNew/deploymentNew.container.js | 4 +- .../flyouts/deploymentNew/deploymentNew.js | 22 +++-- .../flyouts/deploymentNew/deploymentNew.scss | 14 +++- .../packages/flyouts/packageNew/packageNew.js | 63 +++++++++----- .../flyouts/packageNew/packageNew.scss | 13 ++- src/components/shared/pageStats/README.md | 12 +++ src/components/shared/pageStats/index.js | 1 + .../shared/pageStats/statGroup/statGroup.scss | 2 +- .../statPropertyPair/statPropertyPair.js | 16 ++++ .../statPropertyPair/statPropertyPair.scss | 23 +++++ src/store/reducers/deploymentsReducer.js | 1 + 14 files changed, 203 insertions(+), 95 deletions(-) create mode 100644 src/components/shared/pageStats/statPropertyPair/statPropertyPair.js create mode 100644 src/components/shared/pageStats/statPropertyPair/statPropertyPair.scss diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 63ba2bfc7..4b7272f04 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -579,8 +579,8 @@ "priorityPlaceHolder": "Enter priority", "namePlaceHolder": "Enter name", "targetText": "targeted devices", - "infoText": "* This deployment runs continuously. Every edge device (and any you add in the future) in the selected device group will receive this package.", - "successText": "View your deployment status detail for {{deploymentName}}", + "infoText": "This deployment runs continuously. Every edge device (and any you add in the future) in the selected device group will receive this package.", + "successText": "View your deployment status detail for <1><0>{{deploymentName}}.", "creating": "Creating deployment", "validation": { "required": "Is required", @@ -657,7 +657,9 @@ "browseText": "for a package file", "placeHolder": "Select package type", "package": "Package", - "deploymentText": "To deploy packages, go to the 'Deployments' page and click '+ New Deployment' button.", + "deploymentsPage": "Deployments page", + "newDeployment": "+ New Deployment", + "deploymentText": "To deploy packages, go to the <1>Deployments page, and then click <3>+ New Deployment button.", "validation": { "required": "Is required" } diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js index 79bccca1d..94be1b378 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.js @@ -15,7 +15,8 @@ import { RefreshBar, StatSection, StatGroup, - StatProperty + StatProperty, + StatPropertyPair } from 'components/shared'; import { TimeRenderer } from 'components/shared/cellRenderers'; import { getPackageTypeTranslation, svgs } from 'utilities'; @@ -135,20 +136,12 @@ export class DeploymentDetails extends Component {
{name}
- + - -
- {t('deployments.details.priority')} -
-
- {priority} -
-
- - - - - -
- {t('deployments.details.deviceGroup')} -
-
- {deviceGroupName} -
-
- -
- {t('deployments.details.start')} -
-
- {TimeRenderer({ value: createdDateTimeUtc })} -
-
+
- -
- {t('deployments.details.packageType')} -
-
- {type ? getPackageTypeTranslation(type, t) : undefined} -
-
- -
- {t('deployments.details.package')} -
-
- {packageName} -
-
+ +
+
+ + + + + + + + + + + + +
diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.scss b/src/components/pages/deployments/deploymentDetails/deploymentDetails.scss index 662102296..947e9c93c 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.scss +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.scss @@ -10,6 +10,8 @@ flex-grow: 1; .deployment-details-summary-container { + @include rem-fallback(padding-right, 40px); + .deployment-name{ font-weight: 700; @include rem-font-size(34px); @@ -19,28 +21,47 @@ .deployment-details-summary-labels { text-transform: uppercase; @include rem-font-size(12px); - @include rem-fallback(padding-bottom, 10px); } .stat-failed { @include rem-font-size(16px); } .summary-container { @include rem-fallback(padding-top, 30px); } - .summary-container-columns { @include rem-fallback(min-width, 200px); } + .summary-container-columns { + justify-content: space-around; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + @include rem-fallback(height, 50px); + @include rem-fallback(width, 200px); + } + + .summary-container-row1 { @include rem-fallback(padding-top, 30px); } - .summary-container-second-row { @include rem-fallback(padding-top, 20px); } + .summary-container-row2 { @include rem-fallback(padding-top, 20px); } + + .summary-container-succeeded { + line-height: unset; + @include rem-fallback(padding-top, 1px); + @include rem-fallback(padding-bottom, 6px); + } + + .summary-container-pending { + padding-top: 0px; + line-height: unset; + @include rem-fallback(padding-bottom, 3px); + } } .deployment-details-devices-affected { @include rem-fallback(border-top, 1px, solid); @include rem-fallback(padding, 35px, 0px, 0px, 0px); - @include rem-fallback(margin-top, 50px); + @include rem-fallback(margin-top, 30px); } @include themify($themes) { - .deployment-details-summary-values { color: themed('colorContentText'); } - - .deployment-details-summary-labels, .deployment-name { color: themed('colorHeaderText'); } + .deployment-name, + .deployment-details-summary-labels { color: themed('colorHeaderText'); } .stat-failed { fill: themed('colorFailed'); } diff --git a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js index 16f61a0ad..f912fc43d 100644 --- a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js +++ b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.container.js @@ -6,6 +6,7 @@ import { DeploymentNew } from './deploymentNew'; import { getCreateDeploymentError, getCreateDeploymentPendingStatus, + getLastItemId, epics as deploymentsEpics, redux as deploymentsRedux } from 'store/reducers/deploymentsReducer'; @@ -38,7 +39,8 @@ const mapStateToProps = state => ({ devicesPending: getDevicesByConditionPendingStatus(state), devicesError: getDevicesByConditionError(state), createIsPending: getCreateDeploymentPendingStatus(state), - createError: getCreateDeploymentError(state) + createError: getCreateDeploymentError(state), + createdDeploymentId: getLastItemId(state) }); // Wrap the dispatch methods diff --git a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js index e3da27cef..2b9f807ec 100644 --- a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js +++ b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.js @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. import React from 'react'; +import { Trans } from 'react-i18next'; +import { Link } from "react-router-dom"; import { packageTypeOptions, toDiagnosticsModel, toSinglePropertyDiagnosticsModel } from 'services/models'; import { svgs, LinkedComponent, Validator } from 'utilities'; @@ -184,7 +186,8 @@ export class DeploymentNew extends LinkedComponent { packages, deviceGroups, devicesPending, - devicesError + devicesError, + createdDeploymentId } = this.props; const { name, @@ -237,7 +240,7 @@ export class DeploymentNew extends LinkedComponent { type="text" className="long" link={this.nameLink} - onChange={(target) => this.genericOnChange('NewDeployment_NameText', 'Name', target.value)} + onBlur={(event) => this.genericOnChange('NewDeployment_NameText', 'Name', event.target.value)} placeholder={t('deployments.flyouts.new.namePlaceHolder')} /> } { @@ -271,7 +274,7 @@ export class DeploymentNew extends LinkedComponent { disabled={!isPackageTypeSelected} link={this.packageIdLink} options={packageOptions} - onChange={(target) => this.genericOnChange('NewDeployment_PackageSelect', 'Package', target.value.value)} + onChange={(event) => this.genericOnChange('NewDeployment_PackageSelect', 'Package', event.target.value.value)} placeholder={isPackageTypeSelected ? t('deployments.flyouts.new.packagePlaceHolder') : ""} clearable={false} searchable={false} /> @@ -313,7 +316,7 @@ export class DeploymentNew extends LinkedComponent { type="text" className="long" link={this.priorityLink} - onChange={(target) => this.genericOnChange('NewDeployment_PriorityNumber', 'Priority', target.value)} + onBlur={(event) => this.genericOnChange('NewDeployment_PriorityNumber', 'Priority', event.target.value)} placeholder={t('deployments.flyouts.new.priorityPlaceHolder')} /> } { @@ -342,13 +345,22 @@ export class DeploymentNew extends LinkedComponent { {/** Displays a info message if package type selected is edge Manifest */ !changesApplied && edgePackageSelected &&
+ * {t('deployments.flyouts.new.infoText')}
} {/** Displays a success message if deployment is created successfully */ completedSuccessfully &&
- {t('deployments.flyouts.new.successText', { deploymentName: name })} + + View your deployment status detail for + + {{ deploymentName: name }} + + . +
} {/** Displays an error message if one occurs while creating deployment. */ diff --git a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.scss b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.scss index e167259ec..98141f498 100644 --- a/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.scss +++ b/src/components/pages/deployments/deploymentsHome/flyouts/deploymentNew/deploymentNew.scss @@ -7,7 +7,12 @@ .new-deployment-content { @include rem-fallback(padding-top, 24px); - .new-deployment-info-text, + .new-deployment-info-text { + line-height: 1.7; + @include rem-fallback(padding-top, 24px); + @include rem-font-size(14px); + } + .new-deployment-flyout-error { @include rem-fallback(padding-top, 24px); } .new-deployment-formGroup, @@ -21,11 +26,18 @@ @include rem-fallback(margin-left, 8px); } + .new-deployment-info-star { @include rem-font-size(24px); } + @include themify($themes) { .summary-icon svg { fill: themed('colorContentText'); } .new-deployment-flyout-error { border-color: themed('colorAlert'); } .new-deployment-success-labels { color: themed('colorFlyoutText'); } + + .new-deployment-detail-page-link, + .new-deployment-info-text, { color: themed('colorContentText'); } + + .new-deployment-info-star { color: themed('colorAlert'); } } } diff --git a/src/components/pages/packages/flyouts/packageNew/packageNew.js b/src/components/pages/packages/flyouts/packageNew/packageNew.js index 3582f44e7..6a84e0a53 100644 --- a/src/components/pages/packages/flyouts/packageNew/packageNew.js +++ b/src/components/pages/packages/flyouts/packageNew/packageNew.js @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. import React from 'react'; +import { Trans } from 'react-i18next'; +import { Link } from "react-router-dom"; import { packageTypeOptions, @@ -89,7 +91,7 @@ export class PackageNew extends LinkedComponent { render() { const { t, isPending, error } = this.props; - const { packageFile, changesApplied } = this.state; + const { type, packageFile, changesApplied } = this.state; const summaryCount = 1; const typeOptions = packageTypeOptions.map(value => ({ @@ -117,29 +119,38 @@ export class PackageNew extends LinkedComponent { {t('packages.flyouts.new.type')} - + { + !completedSuccessfully && + + } + { + completedSuccessfully && {type} + } -
- - - {t('packages.flyouts.new.browseText')} -
+ { + !completedSuccessfully && +
+ + + {t('packages.flyouts.new.browseText')} +
+ } @@ -152,7 +163,13 @@ export class PackageNew extends LinkedComponent { { completedSuccessfully &&
- {t('packages.flyouts.new.deploymentText')} + + To deploy packages, go to the + {t('packages.flyouts.new.deploymentsPage')} + , and then click + {t('packages.flyouts.new.newDeployment')} + button. +
} {/** Displays an error message if one occurs while applying changes. */ diff --git a/src/components/pages/packages/flyouts/packageNew/packageNew.scss b/src/components/pages/packages/flyouts/packageNew/packageNew.scss index 8ded51940..55ed1e575 100644 --- a/src/components/pages/packages/flyouts/packageNew/packageNew.scss +++ b/src/components/pages/packages/flyouts/packageNew/packageNew.scss @@ -23,10 +23,15 @@ .new-package-hidden-input { display: none; } .new-package-file-name, - .new-package-deployment-text, .new-package-header, .new-package-flyout-error { @include rem-fallback(padding-top, 24px); } + + .new-package-deployment-text { + @include rem-fallback(padding-top, 24px); + line-height: 1.7; + } + .new-package-descr { @include rem-fallback(font-size, 12px); @include rem-fallback(padding-top, 16px); @@ -41,7 +46,11 @@ @include themify($themes) { .new-package-header, .new-package-browse-click, - .new-package-file-name { color: themed('colorContentText'); } + .new-package-file-name, + .new-package-deployment-text, + .new-package-deployment-page-link { color: themed('colorContentText'); } + + .new-package-success-labels { color: themed('colorFlyoutText'); } .new-package-descr svg, .summary-icon svg { fill: themed('colorContentText'); } diff --git a/src/components/shared/pageStats/README.md b/src/components/shared/pageStats/README.md index e7718aebe..299f67485 100644 --- a/src/components/shared/pageStats/README.md +++ b/src/components/shared/pageStats/README.md @@ -15,6 +15,10 @@ A presentational component containing one or many StatPropertys. By default the A presentational component containing number value, label, and an optional svg and svgClassname. The number value can be of three different sizes- large, medium or small, based on `size` parameter. By default the 'size' will be assigned 'small'. +### StatProperty:  + +A presentational component containing label, and a value stacked vertically. + ## Examples:  ```html @@ -33,3 +37,11 @@ A presentational component containing number value, label, and an optional svg a ``` + +```html + + + + + +``` diff --git a/src/components/shared/pageStats/index.js b/src/components/shared/pageStats/index.js index 245525c94..1b0a02e42 100644 --- a/src/components/shared/pageStats/index.js +++ b/src/components/shared/pageStats/index.js @@ -3,3 +3,4 @@ export * from './statGroup/statGroup'; export * from './statSection/statSection'; export * from './statProperty/statProperty'; +export * from './statPropertyPair/statPropertyPair'; diff --git a/src/components/shared/pageStats/statGroup/statGroup.scss b/src/components/shared/pageStats/statGroup/statGroup.scss index 94c76bd44..3880fe181 100644 --- a/src/components/shared/pageStats/statGroup/statGroup.scss +++ b/src/components/shared/pageStats/statGroup/statGroup.scss @@ -5,7 +5,7 @@ .stat-cell { display: flex; flex-flow: column wrap; - justify-content: flex-end; + justify-content: flex-start; flex-shrink: 0; @include rem-fallback(margin-right, 40px); } diff --git a/src/components/shared/pageStats/statPropertyPair/statPropertyPair.js b/src/components/shared/pageStats/statPropertyPair/statPropertyPair.js new file mode 100644 index 000000000..1fc7f0e46 --- /dev/null +++ b/src/components/shared/pageStats/statPropertyPair/statPropertyPair.js @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +import React from 'react'; + +import { joinClasses } from 'utilities'; +import './statPropertyPair.css'; + +/** A presentational component containing statistics value, label and icon */ +export const StatPropertyPair = ({ label, value, className }) => { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/src/components/shared/pageStats/statPropertyPair/statPropertyPair.scss b/src/components/shared/pageStats/statPropertyPair/statPropertyPair.scss new file mode 100644 index 000000000..5fcfe9e28 --- /dev/null +++ b/src/components/shared/pageStats/statPropertyPair/statPropertyPair.scss @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +@import 'src/styles/mixins'; +@import 'src/styles/themes'; + +.stat-property-pair { + display: flex; + flex-flow: column wrap; + justify-content: flex-start; + flex-shrink: 0; + + .stat-property-pair-label { + text-transform: uppercase; + @include rem-font-size(12px); + @include rem-fallback(padding-bottom, 10px); + } + + @include themify($themes) { + .stat-property-pair-label { color: themed('colorHeaderText'); } + + .stat-property-pair-label-value { color: themed('colorContentText'); } + } +} diff --git a/src/store/reducers/deploymentsReducer.js b/src/store/reducers/deploymentsReducer.js index abed19818..fdb27d6ed 100644 --- a/src/store/reducers/deploymentsReducer.js +++ b/src/store/reducers/deploymentsReducer.js @@ -244,4 +244,5 @@ export const getDeployedDevices = createSelector( } }), DeployedDevicesEntities)) ); +export const getLastItemId = state => getItems(state).length > 0 ? getItems(state)[0] : ''; // ========================= Selectors - END From 87e927b8402b46283ca5f8ca1949330093ab6263 Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 9 Oct 2018 16:41:51 -0700 Subject: [PATCH 25/25] Merge master into Edge Feature branch (#1125) * Dev Walkthru: add a new Panel to the Dashboard (#1062) * Dev Walkthru: add a new Panel to the Dashboard * small tweaks, review feedback * fix bad code end marker * Diagnostics bugFix (#1065) * flatMap * Dummy comment to retrigger build * Add Rule Diagnostics (#1064) Add diagnostics logging for rule create/update events. Added the following metrics: Rule_NewClick Rule_EditClick Rule_DeviceGroupClick Rule_CalculationClick Rule_FieldClick Rule_OperatorClick Rule_AddConditionClick Rule_SeverityLevelClick Rule_StatusToggle Rule_ApplyClick Rule_CancelClick Rule_TopXCloseClick Also includes new "sessionid" sections of diagnostics call, which logs the time in ms since Jan 1, 1970 when the page was loaded (amplitude expects session id in this format). This fields will be added by diagnostics to enable logging of session id to amplitude--until those changes go in it will be ignored by the backend. * Delete .travis.yml (#1066) * Delete .travis.yml * Update README.md * Add diagnostics for new device funnel (#1075) * Add device metrics Add metrics for new device flyout * Add device created metric Add metric on device create so we can log device id for physical devices * Fix rule apply event Rule apply click event was only emitted for new rules. Move call so it is emitted if a rule is added or edited. * Address comments and align metric names Address comments. Update metric names to be in same format as rule metrics * fix insertion when entities are null for devices and rules (#1078) * Refactor to make walkthrough code less intrusive (#1069) * refactor to make walkthrough code less intrusive * refactor after meeting with team * update MD files, move httpClient * review feedback * fix nit * update breadcrumbs to use isDef instead of checking undefined * Treat text and number correctly in device jobs (#1082) * treat text and number correctly in device jobs * fix formatting nit * Rule updates need to send ETag (#1084) UI needs to send the ETag when updating rules. * Rule enable/disable needs to update ETag in redux store (#1086) * Small updates for the add page walkthrough (#1089) * Making cloudToDeviceMethod to empty an string (#1090) * rearrange controls on context menus (#1093) * Add links to Time Series Insights from Dashboard and Device Details Page (#1085) Add a Hyperlink shared component Fetch the Time Series Explorer URL from Telemetry Add link to dashboard and device details * Adding curly brackets in json Payload (#1094) * Fix alignment of stats on dashbaord (quick fix) (#1097) * Move Add Condition button below the rule conditions (#1103) * Update section decriptions to be more actionable on Device Details (#1110) * fix shell.test.js (#1116) * unit Test fix --- src/components/mocks/mockApp.js | 2 +- .../deploymentDetails.test.js | 3 ++- .../deploymentsHome/deployments.test.js | 3 ++- .../pages/packages/packages.test.js | 1 + src/components/shell/shell.container.js | 25 ------------------- src/components/shell/shell.test.js | 24 ++++++++++++------ 6 files changed, 22 insertions(+), 36 deletions(-) delete mode 100644 src/components/shell/shell.container.js diff --git a/src/components/mocks/mockApp.js b/src/components/mocks/mockApp.js index dbfdc310f..b8b9a8d4a 100644 --- a/src/components/mocks/mockApp.js +++ b/src/components/mocks/mockApp.js @@ -6,7 +6,7 @@ import { Provider } from 'react-redux'; import { MemoryRouter as Router } from 'react-router-dom'; import i18n from 'i18next'; import { I18nextProvider } from 'react-i18next'; -import configureStore from 'store/configureStore'; +import { configureStore } from 'store/configureStore'; // Initialize internationalization for testing i18n diff --git a/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js b/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js index 7d7b1bb2c..522063f63 100644 --- a/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js +++ b/src/components/pages/deployments/deploymentDetails/deploymentDetails.test.js @@ -14,7 +14,8 @@ describe('Deployment details Component', () => { match: { params: { id: 'testId' } }, fetchDeployment: () => { }, deleteItem: () => { }, - currentDeployment: {} + currentDeployment: {}, + updateCurrentWindow: () => { } }; const wrapper = shallow( diff --git a/src/components/pages/deployments/deploymentsHome/deployments.test.js b/src/components/pages/deployments/deploymentsHome/deployments.test.js index 268ac3744..dc2d75d67 100644 --- a/src/components/pages/deployments/deploymentsHome/deployments.test.js +++ b/src/components/pages/deployments/deploymentsHome/deployments.test.js @@ -11,7 +11,8 @@ describe('Deployments Component', () => { const fakeProps = { t: () => {}, - fetchDeployments: () => {} + fetchDeployments: () => {}, + updateCurrentWindow: () => { } }; const wrapper = shallow( diff --git a/src/components/pages/packages/packages.test.js b/src/components/pages/packages/packages.test.js index 8e42e6367..631b03bca 100644 --- a/src/components/pages/packages/packages.test.js +++ b/src/components/pages/packages/packages.test.js @@ -17,6 +17,7 @@ describe('Packages Component', () => { isPending: false, lastUpdated: undefined, fetchPackages: () => { }, + updateCurrentWindow: () => { }, t: () => { }, }; diff --git a/src/components/shell/shell.container.js b/src/components/shell/shell.container.js deleted file mode 100644 index 03d1487be..000000000 --- a/src/components/shell/shell.container.js +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -import { withRouter } from 'react-router-dom'; -import { connect } from 'react-redux'; -import { translate } from 'react-i18next'; -import { AuthService } from 'services'; -import { - epics as appEpics, - getTheme, - getDeviceGroupFlyoutStatus -} from 'store/reducers/appReducer'; -import Shell from './shell'; - -const mapStateToProps = state => ({ - theme: getTheme(state), - deviceGroupFlyoutIsOpen: getDeviceGroupFlyoutStatus(state) -}); - -// Wrap with the router and wrap the dispatch method -const mapDispatchToProps = dispatch => ({ - registerRouteEvent: pathname => dispatch(appEpics.actions.detectRouteChange(pathname)), - logout: () => AuthService.logout() -}); - -export const ShellContainer = withRouter(translate()(connect(mapStateToProps, mapDispatchToProps)(Shell))); diff --git a/src/components/shell/shell.test.js b/src/components/shell/shell.test.js index daf402328..b6ffc7bb8 100644 --- a/src/components/shell/shell.test.js +++ b/src/components/shell/shell.test.js @@ -1,20 +1,28 @@ + // Copyright (c) Microsoft. All rights reserved. import React from 'react'; import { mount } from 'enzyme'; import 'polyfills'; -import ShellContainer from 'components/shell/shell.container'; -import MockApp from 'components/mocks/mockApp'; +import Shell from 'components/shell/shell'; describe('Shell integration test', () => { - let wrapper; it('Render Shell component', () => { - wrapper = mount( - - - + + const fakeProps = { + theme: 'dark', + deviceGroupFlyoutIsOpen: () => { }, + registerRouteEvent: () => { }, + logout: () => { }, + t: () => { }, + history : { + listen : () => {} + } + }; + + const wrapper = mount( + ); }); - });