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:
Родитель
a55804a70c
Коммит
773b993ff3
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче