-
-
{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') }
-
-
+ {t('maintenance.ruleDetailsDesc')}
+
+
{t('maintenance.ruleDetail')}
+
+
+
{t('maintenance.alertOccurrences')}
+
+
+
{t('maintenance.relatedInfo')}
+
+ {t('maintenance.all')}
- {t('maintenance.devices')}
- {t('maintenance.telemetry')}
+
+ {
+ (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')}
+
+
+
+
+ }
- {
- (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/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..d72499408 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,29 +39,30 @@ export const Summary = ({
onTimeIntervalChange,
timeInterval,
...props
-}) => [
-
-
+}) =>
+
+
+
-
+
+
+
+
-
- ,
-
-
+
- ];
+ ;
diff --git a/src/components/pages/maintenance/summary/summary.scss b/src/components/pages/maintenance/summary/summary.scss
index e18f79593..88b25ce07 100644
--- a/src/components/pages/maintenance/summary/summary.scss
+++ b/src/components/pages/maintenance/summary/summary.scss
@@ -11,6 +11,8 @@
@include rem-font-size(20px);
}
+ .summary-stat-container { @include rem-fallback(padding-bottom, 20px); }
+
@include themify($themes) {
.stat-warning { fill: themed('colorWarning'); }
.stat-critical { fill: themed('colorAlert'); }
diff --git a/src/components/pages/packages/flyouts/index.js b/src/components/pages/packages/flyouts/index.js
new file mode 100644
index 000000000..964310060
--- /dev/null
+++ b/src/components/pages/packages/flyouts/index.js
@@ -0,0 +1,3 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+export * from './packageNew';
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/packageNew/packageNew.container.js b/src/components/pages/packages/flyouts/packageNew/packageNew.container.js
new file mode 100644
index 000000000..95e9f2ea1
--- /dev/null
+++ b/src/components/pages/packages/flyouts/packageNew/packageNew.container.js
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+import { connect } from 'react-redux';
+import { translate } from 'react-i18next';
+import { PackageNew } from './packageNew';
+import {
+ getCreatePackageError,
+ getCreatePackagePendingStatus,
+ 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 => ({
+ isPending: getCreatePackagePendingStatus(state),
+ error: getCreatePackageError(state)
+});
+
+// Wrap the dispatch methods
+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
new file mode 100644
index 000000000..6a84e0a53
--- /dev/null
+++ b/src/components/pages/packages/flyouts/packageNew/packageNew.js
@@ -0,0 +1,212 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+import React from 'react';
+import { Trans } from 'react-i18next';
+import { Link } from "react-router-dom";
+
+import {
+ packageTypeOptions,
+ toSinglePropertyDiagnosticsModel,
+ toDiagnosticsModel
+} 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 './packageNew.css';
+
+const fileInputAccept = ".json,application/json";
+
+export class PackageNew extends LinkedComponent {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ type: undefined,
+ packageFile: undefined,
+ changesApplied: undefined
+ };
+ }
+
+ componentWillUnmount() {
+ this.props.resetPackagesPendingError();
+ }
+
+ apply = (event) => {
+ 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 = () => {
+ return [
+ this.packageTypeLink,
+ ].every(link => !link.error);
+ }
+
+ genericCloseClick = (eventName) => {
+ const { onClose, logEvent } = this.props;
+ logEvent(toDiagnosticsModel(eventName, {}));
+ onClose();
+ }
+
+ render() {
+ const { t, isPending, error } = this.props;
+ const { type, 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')}
+ this.genericCloseClick('NewPackage_CloseClick')} />
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/components/pages/packages/flyouts/packageNew/packageNew.scss b/src/components/pages/packages/flyouts/packageNew/packageNew.scss
new file mode 100644
index 000000000..55ed1e575
--- /dev/null
+++ b/src/components/pages/packages/flyouts/packageNew/packageNew.scss
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+@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-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);
+ @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,
+ .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'); }
+
+ .new-package-flyout-error { border-color: themed('colorAlert'); }
+ }
+}
diff --git a/src/components/pages/packages/modals/deletePackage/deletePackage.container.js b/src/components/pages/packages/modals/deletePackage/deletePackage.container.js
new file mode 100644
index 000000000..d17c899f7
--- /dev/null
+++ b/src/components/pages/packages/modals/deletePackage/deletePackage.container.js
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+import { connect } from 'react-redux';
+import { translate } from 'react-i18next';
+import { DeleteModal } from 'components/shared';
+import {
+ getDeletePackageError,
+ getDeletePackagePendingStatus,
+ epics as packagesEpics
+} from 'store/reducers/packagesReducer';
+import { epics as appEpics } from 'store/reducers/appReducer';
+
+// Pass the global info needed
+const mapStateToProps = state => ({
+ isPending: getDeletePackagePendingStatus(state),
+ error: getDeletePackageError(state)
+});
+
+// Wrap the dispatch methods
+const mapDispatchToProps = dispatch => ({
+ 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/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
new file mode 100644
index 000000000..ac62a5499
--- /dev/null
+++ b/src/components/pages/packages/packages.container.js
@@ -0,0 +1,33 @@
+// 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,
+ getPackagesError,
+ getPackagesLastUpdated,
+ getPackagesPendingStatus
+} from 'store/reducers/packagesReducer';
+import {
+ redux as appRedux,
+ epics as appEpics,
+} from 'store/reducers/appReducer';
+
+// Pass the packages status
+const mapStateToProps = 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()),
+ 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
new file mode 100644
index 000000000..df90e1c15
--- /dev/null
+++ b/src/components/pages/packages/packages.js
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+import React, { Component } from 'react';
+import { permissions, toDiagnosticsModel } from 'services/models';
+import { PackagesGrid } from './packagesGrid';
+import {
+ AjaxError,
+ Btn,
+ ComponentArray,
+ ContextMenu,
+ ContextMenuAlign,
+ PageContent,
+ Protected,
+ RefreshBar,
+ PageTitle
+} from 'components/shared';
+import { PackageNewContainer } 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
+ };
+
+ this.props.updateCurrentWindow('Packages');
+
+ 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
+ this.setState(closedFlyoutState);
+ }
+ }
+
+ closeFlyout = () => {
+ this.props.logEvent(toDiagnosticsModel('Packages_NewClose', {}));
+ this.setState(closedFlyoutState);
+ }
+
+ onContextMenuChange = contextBtns => this.setState({
+ contextBtns,
+ openFlyoutName: undefined
+ });
+
+ openNewPackageFlyout = () => {
+ this.props.logEvent(toDiagnosticsModel('Packages_NewClick', {}));
+ this.setState({
+ openFlyoutName: 'new-Package'
+ });
+ }
+
+ onGridReady = gridReadyEvent => this.packageGridApi = gridReadyEvent.api;
+
+ 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 (
+
+
+
+ { /* 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/packages.scss b/src/components/pages/packages/packages.scss
new file mode 100644
index 000000000..da5440ae5
--- /dev/null
+++ b/src/components/pages/packages/packages.scss
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+@import 'src/styles/variables';
+@import 'src/styles/mixins';
+@import 'src/styles/themes';
+
+.package-container {
+ display: flex;
+ flex-flow: column nowrap;
+ padding: $baseContentPadding;
+}
diff --git a/src/components/pages/packages/packages.test.js b/src/components/pages/packages/packages.test.js
new file mode 100644
index 000000000..631b03bca
--- /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: () => { },
+ updateCurrentWindow: () => { },
+ 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..2c75c5cae
--- /dev/null
+++ b/src/components/pages/packages/packagesGrid/packagesGrid.js
@@ -0,0 +1,115 @@
+// Copyright (c) Microsoft. All rights reserved.
+import React, { Component } from 'react';
+import { permissions } from 'services/models';
+import { packagesColumnDefs, defaultPackagesGridProps } from './packagesGridConfig';
+import { Btn, ComponentArray, PcsGrid, Protected } from 'components/shared';
+import { isFunc, translateColumnDefs, svgs } from 'utilities';
+import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig';
+import { PackageDeleteContainer } from '../modals';
+
+import './packagesGrid.css';
+
+const closedModalState = {
+ openModalName: undefined
+};
+
+export class PackagesGrid extends Component {
+ constructor(props) {
+ super(props);
+
+ // Set the initial state
+ this.state = {
+ ...closedModalState,
+ hardSelectedPackages: []
+ };
+
+ this.columnDefs = [
+ checkboxColumn,
+ packagesColumnDefs.name,
+ packagesColumnDefs.type,
+ packagesColumnDefs.dateCreated
+ ];
+
+ this.contextBtns =
+
+
+ {props.t('packages.delete')}
+
+ ;
+ }
+
+ getOpenModal = () => {
+ if (this.state.openModalName === 'delete-package' && this.state.hardSelectedPackages[0]) {
+ return
+ }
+ return null;
+ }
+
+ /**
+ * 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 === 1 ? this.contextBtns : null);
+ this.setState({
+ hardSelectedPackages: selectedPackages
+ });
+ }
+ if (isFunc(onHardSelectChange)) {
+ onHardSelectChange(selectedPackages);
+ }
+ }
+
+ closeModal = () => this.setState(closedModalState);
+
+ openModal = (modalName) => () => this.setState({
+ openModalName: modalName
+ });
+
+ 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
+ }
+ };
+
+ return (
+
+
+ {this.getOpenModal()}
+
+ );
+ }
+}
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..41407a647
--- /dev/null
+++ b/src/components/pages/packages/packagesGrid/packagesGridConfig.js
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+import Config from 'app.config';
+import { TimeRenderer, SoftSelectLinkRenderer } from 'components/shared/cellRenderers';
+import { gridValueFormatters } from 'components/shared/pcsGrid/pcsGridConfig';
+
+const { checkForEmpty } = gridValueFormatters;
+
+export const packagesColumnDefs = {
+ name: {
+ headerName: 'packages.grid.name',
+ field: 'name',
+ sort: 'asc',
+ valueFormatter: ({ value }) => checkForEmpty(value),
+ cellRendererFramework: SoftSelectLinkRenderer
+ },
+ type: {
+ headerName: 'packages.grid.type',
+ field: 'type',
+ valueFormatter: ({ value }) => checkForEmpty(value)
+ },
+ dateCreated: {
+ headerName: 'packages.grid.dateCreated',
+ field: 'dateCreated',
+ cellRendererFramework: TimeRenderer
+ }
+};
+
+export const defaultPackagesGridProps = {
+ enableColResize: true,
+ multiSelect: true,
+ pagination: true,
+ paginationPageSize: Config.paginationPageSize,
+ rowSelection: 'multiple'
+};
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 d9d95b818..432647639 100644
--- a/src/components/pages/rules/rules.js
+++ b/src/components/pages/rules/rules.js
@@ -8,13 +8,15 @@ import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'compone
import {
AjaxError,
Btn,
+ ComponentArray,
ContextMenu,
ContextMenuAlign,
PageContent,
+ PageTitle,
Protected,
RefreshBar,
SearchInput
- } from 'components/shared';
+} from 'components/shared';
import { NewRuleFlyout } from './flyouts';
import { svgs } from 'utilities';
@@ -48,11 +50,6 @@ export class Rules extends Component {
}
}
- changeDeviceGroup = () => {
- const { changeDeviceGroup, deviceGroups } = this.props;
- changeDeviceGroup(deviceGroups[1].id);
- }
-
closeFlyout = () => this.setState(closedFlyoutState);
openNewRuleFlyout = () => {
@@ -91,28 +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..c3c657038 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;
}
@@ -175,7 +175,7 @@ export class RulesGrid extends Component {
}
}
- getSoftSelectId = ({ id } = {}) => id;
+ getSoftSelectId = ({ id } = '') => id;
closeFlyout = () => this.setState(closedFlyoutState);
@@ -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/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/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/deleteModal/deleteModal.js b/src/components/shared/deleteModal/deleteModal.js
new file mode 100644
index 000000000..ca6ac1e41
--- /dev/null
+++ b/src/components/shared/deleteModal/deleteModal.js
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+import React, { Component } from 'react';
+
+import {
+ AjaxError,
+ Btn,
+ BtnToolbar,
+ Indicator,
+ Modal
+} from 'components/shared';
+import { svgs } from 'utilities';
+import { toSinglePropertyDiagnosticsModel } from 'services/models';
+
+import './deleteModal.css';
+
+export class DeleteModal extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ changesApplied: false
+ };
+ }
+
+ componentWillReceiveProps({ error, isPending, onDelete }) {
+ if (this.state.changesApplied && !error && !isPending) {
+ onDelete();
+ }
+ }
+
+ apply = () => {
+ 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, 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}
+
+
+ {
+ !changesApplied &&
+ {t('modal.delete')}
+ this.genericCloseClick('DeleteModal_CancelClick')}>{t('modal.cancel')}
+
+ }
+ {isPending &&
}
+ {changesApplied && error &&
}
+
+
+ );
+ }
+}
diff --git a/src/components/shared/deleteModal/deleteModal.scss b/src/components/shared/deleteModal/deleteModal.scss
new file mode 100644
index 000000000..5e8ce3b59
--- /dev/null
+++ b/src/components/shared/deleteModal/deleteModal.scss
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+@import 'src/styles/variables';
+@import 'src/styles/mixins';
+@import 'src/styles/themes';
+
+.delete-modal-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/shared/index.js b/src/components/shared/index.js
index 1b8ac07ae..a62416776 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';
@@ -15,3 +16,5 @@ export * from './propertyGrid'
export * from './protected'
export * from './refreshBar/refreshBar';
export * from './svg/svg';
+export * from './modal';
+export * from './deleteModal/deleteModal';
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/components/shared/pageStats/README.md b/src/components/shared/pageStats/README.md
index 3ef8b553d..299f67485 100644
--- a/src/components/shared/pageStats/README.md
+++ b/src/components/shared/pageStats/README.md
@@ -13,7 +13,11 @@ 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'.
+
+### StatProperty:
+
+A presentational component containing label, and a value stacked vertically.
## Examples:
@@ -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/statProperty/statProperty.scss b/src/components/shared/pageStats/statProperty/statProperty.scss
index 3ccd32273..ad6829c5d 100644
--- a/src/components/shared/pageStats/statProperty/statProperty.scss
+++ b/src/components/shared/pageStats/statProperty/statProperty.scss
@@ -7,7 +7,10 @@
display: flex;
flex-flow: row wrap;
flex-shrink: 0;
+ line-height: .85em;
+ align-items: baseline;
+ @include rem-fallback(padding-top, 10px);
.stat-icon {
margin-right: 0.2em;
svg { @include square-px-rem(14px); }
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 (
+
+ );
+}
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/components/shared/pageStats/statSection/statSection.scss b/src/components/shared/pageStats/statSection/statSection.scss
index 98330091e..5e97746d4 100644
--- a/src/components/shared/pageStats/statSection/statSection.scss
+++ b/src/components/shared/pageStats/statSection/statSection.scss
@@ -6,5 +6,5 @@
display: flex;
flex-flow: row wrap;
flex-shrink: 0;
- @include rem-fallback(margin, 30px, 0px);
+
}
diff --git a/src/components/shared/pageTitle/pageTitle.scss b/src/components/shared/pageTitle/pageTitle.scss
index faf4c595e..96f2393fa 100644
--- a/src/components/shared/pageTitle/pageTitle.scss
+++ b/src/components/shared/pageTitle/pageTitle.scss
@@ -1,10 +1,17 @@
// 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 rem-fallback(padding-bottom, 30px);
+
+ @include themify($themes) {
+ color: themed('colorHeaderText');
+ }
}
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);
diff --git a/src/services/configService.js b/src/services/configService.js
index 840798870..c8f5117f0 100644
--- a/src/services/configService.js
+++ b/src/services/configService.js
@@ -6,7 +6,10 @@ import {
prepareLogoResponse,
toDeviceGroupModel,
toDeviceGroupsModel,
- toSolutionSettingThemeModel
+ toSolutionSettingThemeModel,
+ toNewPackageRequestModel,
+ toPackagesModel,
+ toPackageModel
} from './models';
import { Observable } from '../../node_modules/rxjs';
@@ -85,4 +88,28 @@ export class ConfigService {
return HttpClient.put(`${ENDPOINT}solution-settings/theme`, model)
.map(toSolutionSettingThemeModel);
}
+
+ /** Creates a new package */
+ 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 */
+ 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/iotHubManagerService.js b/src/services/iotHubManagerService.js
index c9998dff0..49b855226 100644
--- a/src/services/iotHubManagerService.js
+++ b/src/services/iotHubManagerService.js
@@ -5,7 +5,17 @@ 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,
+ toEdgeAgentsModel
+} from './models';
const ENDPOINT = Config.serviceUrls.iotHubManager;
@@ -58,4 +68,46 @@ export class IoTHubManagerService {
)
.map(([iotResponse, dsResponse]) => toDevicePropertiesModel(iotResponse, dsResponse));
}
+
+ /** Returns deployments */
+ static getDeployments() {
+ return HttpClient.get(`${ENDPOINT}deployments`)
+ .map(toDeploymentsModel);
+ }
+
+ /** Returns deployment */
+ static getDeployment(id) {
+ return HttpClient.get(`${ENDPOINT}deployments/${id}?includeDeviceStatus=true`)
+ .map(toDeploymentModel);
+ }
+
+ /** Queries EdgeAgent */
+ static getModulesByQuery(query) {
+ return HttpClient.post(`${ENDPOINT}modules/query`, query)
+ .map(toEdgeAgentsModel);
+ }
+
+ /** Queries Devices */
+ static getDevicesByQuery(query) {
+ return HttpClient.post(`${ENDPOINT}devices/query`, query)
+ .map(toDevicesModel);
+ }
+
+ /** 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);
+ }
+
+ /** Returns deployments */
+ static getDeploymentDetails(query) {
+ return HttpClient.post(`${ENDPOINT}modules`, query)
+ .map(toDeploymentsModel);
+ }
}
diff --git a/src/services/models/authModels.js b/src/services/models/authModels.js
index ec39d9433..ff2bc9507 100644
--- a/src/services/models/authModels.js
+++ b/src/services/models/authModels.js
@@ -21,7 +21,13 @@ export const permissions = {
createJobs: 'CreateJobs',
- updateSIMManagement: 'UpdateSIMManagement'
+ updateSIMManagement: 'UpdateSIMManagement',
+
+ deletePackages: 'DeletePackages',
+ createPackages: 'CreatePackages',
+
+ createDeployments: 'CreateDeployments',
+ delteDeployments: 'DeleteDeployments'
};
export const toUserModel = (user = {}) => camelCaseReshape(user, {
diff --git a/src/services/models/configModels.js b/src/services/models/configModels.js
index bc4e8c02f..a178033b7 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)) {
@@ -53,3 +53,28 @@ export const toSolutionSettingThemeModel = (response = {}) => camelCaseReshape(r
'diagnosticsOptIn': 'diagnosticsOptIn',
'azureMapsKey': 'azureMapsKey'
});
+
+export const packageTypeOptions = ['EdgeManifest'];
+
+export const toNewPackageRequestModel = ({
+ type,
+ packageFile
+}) => {
+ const data = new FormData();
+ data.append('Type', type);
+ data.append('Package', packageFile);
+ return data;
+}
+
+export const toPackagesModel = (response = {}) => getItems(response)
+ .map(toPackageModel);
+
+export const toPackageModel = (response = {}) => {
+ return camelCaseReshape(response, {
+ 'id': 'id',
+ 'type': 'type',
+ 'name': 'name',
+ 'dateCreated': 'dateCreated',
+ 'content': 'content'
+ });
+};
diff --git a/src/services/models/iotHubManagerModels.js b/src/services/models/iotHubManagerModels.js
index bfcb6ead3..2d0f950d7 100644
--- a/src/services/models/iotHubManagerModels.js
+++ b/src/services/models/iotHubManagerModels.js
@@ -114,7 +114,7 @@ export const toSubmitMethodJobRequestModel = (devices, { jobName, methodName, fi
Firmware: firmwareVersion,
FirmwareUri: firmwareUri
})
- : '{}';
+ : '';
const request = {
JobId: jobId,
QueryCondition: `deviceId in [${deviceIds}]`,
@@ -177,3 +177,50 @@ export const toDevicePropertiesModel = (iotResponse, dsResponse) => {
const propertySet = new Set([...getItems(iotResponse), ...getItems(dsResponse)]);
return [...propertySet];
};
+
+export const toDeploymentModel = (deployment = {}) => {
+ const modelData = camelCaseReshape(deployment, {
+ 'id': 'id',
+ 'name': 'name',
+ 'deviceGroupId': 'deviceGroupId',
+ 'deviceGroupQuery': 'deviceGroupQuery',
+ 'deviceGroupName': 'deviceGroupName',
+ 'packageName': 'packageName',
+ '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);
+
+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
+});
+
+export const toEdgeAgentModel = (edgeAgent = {}) => camelCaseReshape(edgeAgent, {
+ 'deviceId': 'id',
+ 'reported.lastDesiredStatus.description': 'description',
+ 'reported.lastDesiredStatus.code': 'code',
+ 'reported.systemModules.edgeAgent.lastStartTimeUtc': 'start',
+ 'reported.systemModules.edgeAgent.lastExitTimeUtc': 'end'
+});
+
+export const toEdgeAgentsModel = (response = []) => getItems(response)
+ .map(toEdgeAgentModel);
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);
}
diff --git a/src/store/reducers/deploymentsReducer.js b/src/store/reducers/deploymentsReducer.js
new file mode 100644
index 000000000..fdb27d6ed
--- /dev/null
+++ b/src/store/reducers/deploymentsReducer.js
@@ -0,0 +1,248 @@
+// 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 dot from 'dot-object';
+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 }));
+
+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 all Deployments */
+ fetchDeployments: {
+ type: 'DEPLOYMENTS_FETCH',
+ epic: fromAction =>
+ IoTHubManagerService.getDeployments()
+ .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.getModulesByQuery(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',
+ 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);
+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);
+ if (state.entities) {
+ return update(state, {
+ entities: { deployments: { $merge: deployments } },
+ items: { $splice: [[0, 0, result]] },
+ ...setPending(fromAction.type, false)
+ });
+ }
+ return update(state, {
+ entities: { deployments: { $set: deployments } },
+ items: { $set: [result] },
+ ...setPending(fromAction.type, false)
+ });
+};
+
+const deleteDeploymentReducer = (state, { fromAction }) => {
+ const idx = state.items.indexOf(fromAction.payload);
+ return update(state, {
+ entities: { deployments: { $unset: [fromAction.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: { 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,
+ device: normalizedDevices[deviceId]
+ }
+ }),
+ normalizedModules
+ );
+ return update(state, {
+ entities: {
+ deployedDevices: { $set: deployedDevices }
+ },
+ ...setPending(fromAction.type, false)
+ });
+};
+
+const resetDeployedDevicesReducer = (state) => update(state, {
+ entities: {
+ $unset: ['deployedDevices']
+ }
+});
+
+
+/* Action types that cause a pending flag */
+const fetchableTypes = [
+ epics.actionTypes.fetchDeployment,
+ epics.actionTypes.fetchDeployments,
+ epics.actionTypes.createDeployment,
+ 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 },
+ resetDeployedDevices: { type: 'DEPLOYMETS_RESET_DEPLOYED_DEVICES', reducer: resetDeployedDevicesReducer },
+ 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 getDeploymentsEntities = state => getEntities(state).deployments || {};
+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(
+ getDeploymentsEntities, getItems, getActiveDeviceGroupId, getActiveDeviceGroupConditions,
+ (deployments, items, deviceGroupId, deviceGroupConditions = []) =>
+ items.reduce((acc, id) => {
+ const deployment = deployments[id];
+ const activeDeviceGroup = deviceGroupConditions.length > 0 ? deviceGroupId : false;
+ 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))
+);
+export const getLastItemId = state => getItems(state).length > 0 ? getItems(state)[0] : '';
+// ========================= Selectors - END
diff --git a/src/store/reducers/devicesReducer.js b/src/store/reducers/devicesReducer.js
index e8ce1b8f3..d4081f6fd 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,27 @@ 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))
+ }
+ },
+
+ /** 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',
@@ -81,16 +103,17 @@ 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 },
- items: { $splice: [[0, 0, result]] }
+ items: { $splice: [[0, 0, ...result]] }
});
}
return update(state, {
entities: { $set: devices },
- items: { $set: [result] }
+ items: { $set: result }
});
};
@@ -112,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));
@@ -132,7 +166,9 @@ const updatePropertiesReducer = (state, { payload }) => {
/* Action types that cause a pending flag */
const fetchableTypes = [
- epics.actionTypes.fetchDevices
+ epics.actionTypes.fetchDevices,
+ epics.actionTypes.fetchDevicesByCondition,
+ epics.actionTypes.fetchEdgeAgent
];
export const redux = createReducerScenario({
@@ -143,6 +179,8 @@ 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 }
});
export const reducer = { devices: redux.getReducer(initialState) };
@@ -157,8 +195,25 @@ 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])
);
+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/store/reducers/packagesReducer.js b/src/store/reducers/packagesReducer.js
new file mode 100644
index 000000000..729834b73
--- /dev/null
+++ b/src/store/reducers/packagesReducer.js
@@ -0,0 +1,139 @@
+// 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,
+ resetPendingAndErrorReducer,
+ 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 package */
+ deletePackage: {
+ 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, fromAction }) => {
+ const { entities: { packages }, result } = normalize({...payload, isNew: true}, 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: { $set: packages },
+ items: { $set: [result] },
+ ...setPending(fromAction.type, false)
+ });
+};
+
+const deletePackageReducer = (state, { payload, fromAction }) => {
+ const idx = state.items.indexOf(payload);
+ return update(state, {
+ entities: { $unset: [payload] },
+ items: { $splice: [[idx, 1]] },
+ ...setPending(fromAction.type, false)
+ });
+};
+
+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,
+ epics.actionTypes.createPackage,
+ epics.actionTypes.deletePackage
+];
+
+export const redux = createReducerScenario({
+ insertPackage: { type: 'PACKAGE_INSERT', reducer: insertPackageReducer },
+ 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 }
+});
+
+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 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])
+);
+// ========================= Selectors - END
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/store/rootEpic.js b/src/store/rootEpic.js
index 8a14b196a..cf0ecd58f 100644
--- a/src/store/rootEpic.js
+++ b/src/store/rootEpic.js
@@ -5,13 +5,17 @@ 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';
// Extract the epic function from each property object
const epics = [
...appEpics.getEpics(),
+ ...deploymentsEpics.getEpics(),
...devicesEpics.getEpics(),
+ ...packagesEpics.getEpics(),
...rulesEpics.getEpics(),
...simulationEpics.getEpics()
];
diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js
index ce0f6c560..083b7ec2b 100644
--- a/src/store/rootReducer.js
+++ b/src/store/rootReducer.js
@@ -4,13 +4,17 @@ 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 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,
...simulationReducer
});
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)
diff --git a/src/styles/_themes.scss b/src/styles/_themes.scss
index 52e3a3da5..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,
@@ -32,6 +33,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',
@@ -164,6 +167,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
@@ -192,6 +202,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',
@@ -324,5 +336,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
)
);
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/httpClient.js b/src/utilities/httpClient.js
index 595229347..a5b2eb130 100644
--- a/src/utilities/httpClient.js
+++ b/src/utilities/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/utilities/methods.js b/src/utilities/methods.js
index f252e516e..ef43311e6 100644
--- a/src/utilities/methods.js
+++ b/src/utilities/methods.js
@@ -99,6 +99,27 @@ 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');
+ }
+}
+
+/** 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');
diff --git a/src/utilities/svgs.js b/src/utilities/svgs.js
index c690a62f3..28b387749 100644
--- a/src/utilities/svgs.js
+++ b/src/utilities/svgs.js
@@ -25,12 +25,15 @@ 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';
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';
@@ -46,6 +49,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';
@@ -58,7 +62,9 @@ export const svgs = {
tabs: {
dashboard: TabDashboardIconPath,
devices: TabDevicesIconPath,
+ deployments: TabDeploymentsIconPath,
maintenance: TabMaintenanceIconPath,
+ packages: TabPackagesIconPath,
rules: TabRulesIconPath,
example: InfoBubbleIconPath
},
@@ -89,6 +95,8 @@ export const svgs = {
ellipsis: EllipsisIconPath,
enableToggle: EnableToggleIconPath,
error: ErrorIconPath,
+ failed: FailedIconPath,
+ glimmer: GlimmerIconPath,
hamburger: HamburgerIconPath,
info: InfoIconPath,
infoBubble: InfoBubbleIconPath,
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..578bd56d6 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')}
+ ;
}
/**
@@ -92,7 +93,7 @@ export class ExampleGrid extends Component {
}
}
- getSoftSelectId = ({ id } = {}) => id;
+ getSoftSelectId = ({ id } = '') => id;
render() {
const gridProps = {
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 && }
+
+
+ );
}
}