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
This commit is contained in:
Jill Bender 2018-09-07 17:33:26 -07:00 коммит произвёл GitHub
Родитель a55804a70c
Коммит 773b993ff3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 185 добавлений и 18 удалений

Просмотреть файл

@ -138,7 +138,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",
@ -155,7 +156,8 @@
"notConnected": "Offline"
},
"telemetry": {
"header": "Telemetry"
"header": "Telemetry",
"exploreTimeSeries": "Explore in Time Series Insights"
}
}
},
@ -222,7 +224,8 @@
"noneExist": "No tags found for this device."
},
"telemetry": {
"title": "Telemetry"
"title": "Telemetry",
"exploreTimeSeries": "Explore in Time Series Insights"
}
},
"delete": {

Просмотреть файл

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

Просмотреть файл

@ -274,6 +274,7 @@ export class Dashboard extends Component {
const {
theme,
timeInterval,
timeSeriesExplorerUrl,
azureMapsKey,
azureMapsKeyError,
@ -324,6 +325,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,
@ -403,6 +410,7 @@ export class Dashboard extends Component {
</Cell>
<Cell className="col-6">
<TelemetryPanel
timeSeriesExplorerUrl={timeSeriesParamUrl}
telemetry={telemetry}
isPending={telemetryIsPending}
lastRefreshed={lastRefreshed}
@ -413,6 +421,7 @@ export class Dashboard extends Component {
</Cell>
<Cell className="col-4">
<AnalyticsPanel
timeSeriesExplorerUrl={timeSeriesParamUrl}
topAlerts={topAlertsWithName}
alertsPerDeviceId={alertsPerDeviceType}
criticalAlertsChange={criticalAlertsChange}

Просмотреть файл

@ -3,7 +3,7 @@
import React, { Component } from 'react';
import 'tsiclient';
import { AjaxError, Indicator } from 'components/shared';
import { AjaxError, Indicator, Hyperlink } from 'components/shared';
import {
Panel,
PanelHeader,
@ -93,7 +93,7 @@ export class AnalyticsPanel extends Component {
}
render() {
const { t, isPending, criticalAlertsChange, alertsPerDeviceId, topAlerts, error } = this.props;
const { t, isPending, criticalAlertsChange, alertsPerDeviceId, topAlerts, timeSeriesExplorerUrl, error } = this.props;
const showOverlay = isPending && !criticalAlertsChange;
return (
<Panel>
@ -102,6 +102,10 @@ export class AnalyticsPanel extends Component {
{ !showOverlay && isPending && <Indicator size="small" /> }
</PanelHeader>
<PanelContent className="analytics-panel-container">
{
timeSeriesExplorerUrl &&
<Hyperlink className="time-series-explorer" href={timeSeriesExplorerUrl} target="_blank">{t('dashboard.panels.analytics.exploreTimeSeries')}</Hyperlink>
}
<div className="analytics-cell full-width">
<div className="analytics-header">{t('dashboard.panels.analytics.topRule')}</div>
<div className="chart-container" id={barChartId} />

Просмотреть файл

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

Просмотреть файл

@ -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 (
<Panel>
@ -29,6 +29,10 @@ export class TelemetryPanel extends Component {
{ !showOverlay && isPending && <Indicator size="small" /> }
</PanelHeader>
<PanelContent className="telemetry-panel-container">
{
timeSeriesExplorerUrl &&
<Hyperlink className="time-series-explorer" href={timeSeriesExplorerUrl} target="_blank">{t('dashboard.panels.telemetry.exploreTimeSeries')}</Hyperlink>
}
<TelemetryChart telemetry={telemetry} theme={theme} colors={colors} />
{
!showOverlay && Object.keys(telemetry).length === 0

Просмотреть файл

@ -7,3 +7,8 @@
display: flex;
flex-flow: column nowrap;
}
.time-series-explorer {
display: block;
@include rem-fallback(padding, 0px, 0px, 10px, 0px);
}

Просмотреть файл

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

Просмотреть файл

@ -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 (
<Flyout.Container>
<Flyout.Header>
@ -210,7 +218,11 @@ export class DeviceDetails extends Component {
<Section.Container>
<Section.Header>{t('devices.flyouts.details.telemetry.title')}</Section.Header>
<Section.Content>
<TelemetryChart telemetry={telemetry} theme={theme} colors={chartColorObjects} />
{
timeSeriesExplorerUrl &&
<Hyperlink className="time-series-explorer" href={timeSeriesParamUrl} target="_blank">{t('devices.flyouts.details.telemetry.exploreTimeSeries')}</Hyperlink>
}
<TelemetryChart className="telemetry-chart" telemetry={telemetry} theme={theme} colors={chartColorObjects} />
</Section.Content>
</Section.Container>

Просмотреть файл

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

Просмотреть файл

@ -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 (
<a {...props} className={joinClasses('hyperlink', className)} >
{children}
</a>
);
};
Hyperlink.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
href: PropTypes.string,
target: PropTypes.string
};

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -73,6 +73,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,
@ -225,6 +233,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,

Просмотреть файл

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