diff --git a/package.json b/package.json
index 9493a56..2a9dc24 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
"inquirer": "^7.3.3",
"inquirer-checkbox-plus-prompt": "^1.0.1",
"node-fetch": "^2.6.1",
- "prettier": "^1.19.1",
+ "prettier": "^3.0.0",
"querystring": "^0.2.0",
"sharp": "^0.29.2",
"xmldoc": "^1.1.2"
diff --git a/template/next.config.js b/template/next.config.js
index 982410a..78b50f7 100644
--- a/template/next.config.js
+++ b/template/next.config.js
@@ -17,8 +17,9 @@ limitations under the License.
const fs = require('fs');
const path = require('path');
const settings = Object.assign({}, require('./package.json').dashpub.settings);
+const withVideos = require('next-videos');
-module.exports = {
+module.exports = withVideos({
webpack(config, { buildId, webpack }) {
const snapshotPath = path.join(__dirname, 'src/pages/api/data/_snapshot.json');
if (!fs.existsSync(snapshotPath)) {
@@ -40,4 +41,4 @@ module.exports = {
return config;
},
-};
+});
diff --git a/template/package.json b/template/package.json
index 1dacbd4..898c5dc 100644
--- a/template/package.json
+++ b/template/package.json
@@ -5,67 +5,45 @@
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.9.2",
- "@splunk/charting-bundle": "24.5.0",
- "@splunk/dashboard-context": "24.5.0",
- "@splunk/dashboard-core": "24.5.0",
- "@splunk/dashboard-definition": "24.5.0",
- "@splunk/dashboard-event-handlers": "24.5.0",
- "@splunk/dashboard-icons": "24.5.0",
- "@splunk/dashboard-inputs": "24.5.0",
- "@splunk/dashboard-layouts": "24.5.0",
- "@splunk/dashboard-presets": "24.5.0",
- "@splunk/dashboard-telemetry": "24.5.0",
- "@splunk/dashboard-ui": "24.5.0",
- "@splunk/dashboard-utils": "24.5.0",
- "@splunk/dashboard-visualizations": "24.5.0",
- "@splunk/datasource-utils": "24.5.0",
- "@splunk/datasources": "24.5.0",
- "@splunk/react-icons": "3.2.0",
- "@splunk/react-ui": "4.7.0",
- "@splunk/visualization-color-palettes": "24.5.0",
- "@splunk/visualization-context": "24.5.0",
- "@splunk/visualization-encoding": "24.5.0",
- "@splunk/visualization-encoding-parsers": "24.5.0",
- "@splunk/visualization-icons": "24.5.0",
- "@splunk/visualization-themes": "24.5.0",
- "@splunk/visualizations": "24.5.0",
- "@splunk/visualizations-shared": "24.5.0",
+ "@splunk/charting-bundle": "25.9.0",
+ "@splunk/dashboard-context": "26.0.1",
+ "@splunk/dashboard-core": "26.0.1",
+ "@splunk/dashboard-definition": "26.0.1",
+ "@splunk/dashboard-event-handlers": "26.0.1",
+ "@splunk/dashboard-icons": "26.0.1",
+ "@splunk/dashboard-inputs": "26.0.1",
+ "@splunk/dashboard-layouts": "26.0.1",
+ "@splunk/dashboard-presets": "26.0.1",
+ "@splunk/dashboard-telemetry": "26.0.1",
+ "@splunk/dashboard-ui": "26.0.1",
+ "@splunk/dashboard-utils": "26.0.1",
+ "@splunk/dashboard-visualizations": "25.9.0",
+ "@splunk/datasource-utils": "26.0.1",
+ "@splunk/datasources": "26.0.1",
+ "@splunk/react-icons": "^3.3.1",
+ "@splunk/react-ui": "4.16.3",
+ "@splunk/visualization-color-palettes": "25.8.1",
+ "@splunk/visualization-context": "25.8.1",
+ "@splunk/visualization-encoding": "25.8.1",
+ "@splunk/visualization-encoding-parsers": "25.8.1",
+ "@splunk/visualization-icons": "25.8.1",
+ "@splunk/visualization-themes": "25.8.1",
+ "@splunk/visualizations": "25.8.1",
+ "@splunk/visualizations-shared": "25.8.1",
"fast-text-encoding": "^1.0.2",
+ "fullscreen-react": "^1.0.4",
+ "interweave": "^13.1.0",
"next": "^9.3.6",
- "prettier": "^2.0.5",
+ "next-videos": "^1.4.1",
+ "prettier": "^2.8.8",
"querystring": "^0.2.0",
"react": "16.14.0",
+ "react-component-rotator": "^0.1.1",
"react-dom": "16.14.0",
"react-full-screen": "^0.2.4",
+ "react-html-parser": "^2.0.2",
"styled-components": "^5.0.0"
},
- "resolutions": {
- "@splunk/charting-bundle": "24.5.0",
- "@splunk/dashboard-context": "24.5.0",
- "@splunk/dashboard-core": "24.5.0",
- "@splunk/dashboard-definition": "24.5.0",
- "@splunk/dashboard-event-handlers": "24.5.0",
- "@splunk/dashboard-icons": "24.5.0",
- "@splunk/dashboard-inputs": "24.5.0",
- "@splunk/dashboard-layouts": "24.5.0",
- "@splunk/dashboard-presets": "24.5.0",
- "@splunk/dashboard-telemetry": "24.5.0",
- "@splunk/dashboard-ui": "24.5.0",
- "@splunk/dashboard-utils": "24.5.0",
- "@splunk/dashboard-visualizations": "24.5.0",
- "@splunk/datasource-utils": "24.5.0",
- "@splunk/datasources": "24.5.0",
- "@splunk/react-icons": "3.2.0",
- "@splunk/react-ui": "4.7.0",
- "@splunk/visualization-color-palettes": "24.5.0",
- "@splunk/visualization-context": "24.5.0",
- "@splunk/visualization-encoding": "24.5.0",
- "@splunk/visualization-encoding-parsers": "24.5.0",
- "@splunk/visualization-icons": "24.5.0",
- "@splunk/visualization-themes": "24.5.0",
- "@splunk/visualizations": "24.5.0",
- "@splunk/visualizations-shared": "24.5.0"
- },
"scripts": {
"dev": "next dev",
"build": "next build",
diff --git a/template/public/video_assist_preparing.mov b/template/public/video_assist_preparing.mov
new file mode 100644
index 0000000..d7e31c5
Binary files /dev/null and b/template/public/video_assist_preparing.mov differ
diff --git a/template/public/video_inspector_running.mov b/template/public/video_inspector_running.mov
new file mode 100644
index 0000000..86a8bd2
Binary files /dev/null and b/template/public/video_inspector_running.mov differ
diff --git a/template/src/components/dashboard.js b/template/src/components/dashboard.js
index 5437e71..7f8f822 100644
--- a/template/src/components/dashboard.js
+++ b/template/src/components/dashboard.js
@@ -15,18 +15,17 @@ limitations under the License.
*/
import { DashboardContextProvider } from '@splunk/dashboard-context';
-import GeoRegistry from '@splunk/dashboard-context/GeoRegistry';
-import GeoJsonProvider from '@splunk/dashboard-context/GeoJsonProvider';
+import { GeoJsonProvider, GeoRegistry } from '@splunk/dashboard-context';
import DashboardCore from '@splunk/dashboard-core';
import React, { Suspense, useMemo, useEffect, useRef } from 'react';
import Loading from './loading';
import defaultPreset from '../preset';
import { SayCheese, registerScreenshotReadinessDep } from '../ready';
import { testTileConfig } from '@splunk/visualization-context/MapContext';
+import Fullscreen from '@splunk/react-icons/Fullscreen';
const mapTileConfig = { defaultTileConfig: testTileConfig };
-
const PROD_SRC_PREFIXES = [
// Add URL prefixes here that will be replaced with the page's current origin
];
@@ -110,18 +109,44 @@ export default function Dashboard({ definition, preset, width = '100vw', height
};
}, []);
+ const toggleFullSceen = () => {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen();
+ document.body.style.cursor = 'none';
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ }
+ }
+ };
+
return (
-
- }>
-
-
-
-
+ <>
+ toggleFullSceen()}
+ >
+
+ }>
+
+
+
+
+ >
);
}
diff --git a/template/src/components/home.js b/template/src/components/home.js
index 6a96f0a..3865f90 100644
--- a/template/src/components/home.js
+++ b/template/src/components/home.js
@@ -18,6 +18,7 @@ import React from 'react';
import styled from 'styled-components';
import { variables } from '@splunk/themes';
import dashboardManifest from '../_dashboards.json';
+import Chip from '@splunk/react-ui/Chip';
const Wrapper = styled.div`
width: 100vw;
@@ -37,7 +38,8 @@ const DashLink = styled.a`
align-items: center;
justify-content: center;
text-align: center;
- width: 280px;
+ width: 500px;
+
height: 80px;
border: 1px solid #eee;
margin: 10px 10px 0 0;
@@ -59,8 +61,9 @@ export default function Home({ title = 'Dashboards' }) {
Dashboards
{Object.keys(dashboardManifest).map((k) => (
-
- {dashboardManifest[k]}
+
+ {dashboardManifest[k].replace(' - Demo', '')}
+ {dashboardManifest[k].includes('Demo') ? Demo : <>>}
))}
diff --git a/template/src/components/loading.js b/template/src/components/loading.js
index 27f0720..abe323a 100644
--- a/template/src/components/loading.js
+++ b/template/src/components/loading.js
@@ -37,7 +37,17 @@ export default function Loading() {
return (
- Loading...
+
+ {' '}
+
+
);
diff --git a/template/src/pages/[dashboard].jsx b/template/src/pages/[dashboard].jsx
index 3cfaac6..b298d83 100644
--- a/template/src/pages/[dashboard].jsx
+++ b/template/src/pages/[dashboard].jsx
@@ -12,7 +12,7 @@ export default function DashboardPage({ definition, dashboardId, baseUrl }) {
description={definition.description}
imageUrl={`/screens/${dashboardId}.png`}
path={`/${dashboardId}`}
- backgroundColor={definition.layout.options.backgroundColor}
+ backgroundColor={definition.layout.options.backgroundColor ? definition.layout.options.backgroundColor : '#000'}
theme={definition.theme}
baseUrl={baseUrl}
>
diff --git a/template/src/pages/advancedrotator/index.jsx b/template/src/pages/advancedrotator/index.jsx
new file mode 100755
index 0000000..ae58708
--- /dev/null
+++ b/template/src/pages/advancedrotator/index.jsx
@@ -0,0 +1,219 @@
+import React, { lazy, Suspense, useState, useEffect } from 'react';
+import Loading from '../../components/loading';
+import NoSSR from '../../components/nossr';
+import Page from '../../components/page';
+import { useRouter } from 'next/router';
+import Fullscreen from '@splunk/react-icons/Fullscreen';
+
+//This rotator takes following parameters:
+//pages: comma separated values. This expects the following parameters, pages and timer:
+// type, id
+
+//type: either video, or dashboard
+//id: either the dashboard id, the movie file hosted in this project
+
+//timer: Note: for video files, we will simply play the entire video and ignore this. For dashboards, this is how long to show the dashboard, in ms.
+
+//So for example:
+//https://yourproject.vercel.app/advancedrotator?pages=video,movie.mov,90000,swag_store,dashboard,5000
+
+const Dashboard = lazy(() => import('../../components/dashboard'));
+
+export default function DashboardPage({}) {
+ //These are the URL params
+ const { query } = useRouter();
+
+ //This let's us know the timer is ticking
+ const [ticking, setTicking] = useState(true);
+
+ //This is the current count of the timer
+ const [count, setCount] = useState(0);
+
+ //This is the value of the timer. For example "how long do we want each dashboard to show"
+ const [timerValue, setTimerValue] = useState(0);
+
+ //These are the final pages that will be shown
+ const [finalpages, setFinalPages] = useState([[]]);
+
+ //This is the index of the current page
+ const [currPageIndex, setCurrPageIndex] = useState(0);
+
+ //This is the current type of page
+ const [currType, setCurrType] = useState('dashboard');
+
+ //This let's us know a video is done playing
+ const [videoPlayOver, setVideoPlayOver] = useState(false);
+
+ useEffect(() => {
+ console.log(currType);
+
+ if (currType == 'dashboard') {
+ if (count > timerValue) {
+ setCount(0);
+ if (currPageIndex == finalpages.length - 1) {
+ setCurrPageIndex(0);
+ setCurrType(finalpages[0][1]);
+ } else {
+ setCurrPageIndex(currPageIndex + 1);
+ setCurrType(finalpages[currPageIndex + 1][1]);
+ }
+ }
+ }
+
+ if (currType == 'video') {
+ if (videoPlayOver) {
+ setCount(0);
+ if (currPageIndex == finalpages.length - 1) {
+ setCurrPageIndex(0);
+ setCurrType(finalpages[0][1]);
+ setVideoPlayOver(false);
+ } else {
+ setCurrPageIndex(currPageIndex + 1);
+ setCurrType(finalpages[currPageIndex + 1][1]);
+ setVideoPlayOver(false);
+ }
+ }
+ }
+ const timer = setTimeout(() => ticking && setCount(count + 1), 1e3);
+ return () => clearTimeout(timer);
+ }, [count, ticking]);
+
+ useEffect(() => {
+ setTimerValue(query.timer / 1000);
+ }, [query.timer]);
+
+ useEffect(() => {
+ if (typeof query.pages != 'undefined') {
+ var dashboards = [];
+ var dashboards = query.pages.split(',');
+ setCurrType(dashboards[0]);
+
+ var iterator = 0;
+
+ var pages = [];
+ var new_obj = {};
+ for (var dashboard in dashboards) {
+ if (iterator == 2) {
+ iterator = 0;
+ }
+
+ if (iterator == 0) {
+ new_obj['type'] = dashboards[dashboard];
+ }
+ if (iterator == 1) {
+ new_obj.id = dashboards[dashboard];
+ pages.push(new_obj);
+ new_obj = {};
+ }
+ iterator = iterator + 1;
+ }
+
+ var baseUrl = `https://${process.env.VERCEL_URL}`;
+
+ var temp_pages = [];
+
+ for (var page in pages) {
+ if (pages[page].type == 'dashboard') {
+ var definition = require(`../../dashboards/${pages[page].id}/definition.json`);
+ console.log(definition);
+
+ temp_pages.push([
+
+
+ }>
+
+
+
+ ,
+ 'dashboard',
+ ]);
+ }
+
+ if (pages[page].type == 'video') {
+ temp_pages.push([
+
+
+

+
+
+ {' '}
+
+ ,
+ 'video',
+ ]);
+ }
+ }
+ setFinalPages(temp_pages);
+ console.log(temp_pages);
+ }
+ }, [query.pages]);
+
+ const toggleFullSceen = () => {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen();
+ document.body.style.cursor = 'none';
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ }
+ }
+ };
+
+ if (!query.pages || !query.timer) {
+ return <>>;
+ } else {
+ return (
+ <>
+ toggleFullSceen()}
+ >
+
+
+ {finalpages[currPageIndex][0]}
+
+ >
+ );
+ }
+}
diff --git a/template/src/pages/error.jsx b/template/src/pages/error.jsx
new file mode 100755
index 0000000..d03bdd0
--- /dev/null
+++ b/template/src/pages/error.jsx
@@ -0,0 +1,48 @@
+import React from "react";
+import Page from "../components/page";
+
+export default function Home({}) {
+ return (
+
+
+

+
+
+ {" "}
+
{" "}
+
+ We are currently working on this dashboard
+
+
+
+ );
+}
diff --git a/template/src/pages/rotator/index.jsx b/template/src/pages/rotator/index.jsx
new file mode 100755
index 0000000..e13d1f4
--- /dev/null
+++ b/template/src/pages/rotator/index.jsx
@@ -0,0 +1,99 @@
+import React, { lazy, Suspense, useState } from "react";
+import Loading from "../../components/loading";
+import NoSSR from "../../components/nossr";
+import Page from "../../components/page";
+import { useRouter } from "next/router";
+import ComponentRotator from "react-component-rotator";
+import FullscreenLight from "@splunk/react-icons/FullscreenLight";
+
+const Dashboard = lazy(() => import("../../components/dashboard"));
+
+export default function DashboardPage({ dashboardId, baseUrl }) {
+ const { query } = useRouter();
+
+ const toggleFullSceen = () => {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen();
+ document.body.style.cursor = "none";
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ }
+ }
+ };
+
+ if (!query.dashboards || !query.timer) {
+ return null;
+ } else {
+ var dashboards = [];
+ var dashboards = query.dashboards.split(",");
+
+ var defs = {};
+
+ var baseUrl = `https://${process.env.VERCEL_URL}`;
+
+ for (var dashboard in dashboards) {
+ defs[
+ dashboards[dashboard]
+ ] = require(`../../dashboards/${dashboards[dashboard]}/definition.json`);
+ }
+
+ var pages = [];
+ for (var def in defs) {
+ pages.push(
+
+
+ }>
+
+
+
+
+ );
+ }
+ return (
+ <>
+ toggleFullSceen()}
+ >
+
+
+
+ >
+ );
+ }
+}
diff --git a/template/src/pages/timelapse.jsx b/template/src/pages/timelapse.jsx
new file mode 100644
index 0000000..3423101
--- /dev/null
+++ b/template/src/pages/timelapse.jsx
@@ -0,0 +1,36 @@
+import React, { lazy, Suspense, useEffect, useState } from "react";
+import { useRouter } from "next/router";
+
+import Loading from "../components/loading";
+import NoSSR from "../components/nossr";
+import Page from "../components/page";
+
+const TimelapseDashboard = lazy(() => import("../timelapse"));
+
+export default function DashboardPage() {
+ const { query } = useRouter();
+
+ const [dashboardDef, setDashboardDef] = useState({});
+
+ useEffect(() => {
+ if (typeof query.dashboard != "undefined") {
+ var definition = require(`../dashboards/${query.dashboard}/definition.json`);
+ setDashboardDef(definition);
+ }
+ }, [query]);
+ return (
+
+
+ }>
+
+
+
+
+ );
+}
diff --git a/template/src/preset.js b/template/src/preset.js
index b54816c..0db168e 100644
--- a/template/src/preset.js
+++ b/template/src/preset.js
@@ -17,7 +17,10 @@ limitations under the License.
import React, { lazy } from 'react';
import CdnDataSource from './datasource';
import DrilldownHandler from './drilldown';
+import TestDataSource from '@splunk/datasources/TestDataSource';
import { polyfillTextDecoder } from './polyfills';
+import { AbsoluteLayoutViewer } from '@splunk/dashboard-layouts';
+import { SetToken } from '@splunk/dashboard-event-handlers';
import { DropdownInput, TimeRangeInput, MultiselectInput, TextInput, NumberInput } from '@splunk/dashboard-inputs';
const fixRequestParams = (LazyComponent) => (props) => {
@@ -41,13 +44,15 @@ const lazyViz = (fn) => {
const PRESET = {
layouts: {
- absolute: lazyViz(() => import('@splunk/dashboard-layouts/AbsoluteLayoutViewer')),
+ absolute: AbsoluteLayoutViewer,
},
dataSources: {
'ds.cdn': CdnDataSource,
+ 'ds.test': TestDataSource,
},
eventHandlers: {
'drilldown.customUrl': DrilldownHandler,
+ 'drilldown.setToken': SetToken,
},
visualizations: {
// legacy
@@ -103,7 +108,7 @@ const PRESET = {
'splunk.map': commonFlags(lazyViz(() => import('@splunk/visualizations/Map'))),
'splunk.table': commonFlags(lazyViz(() => import('@splunk/visualizations/Table'))),
},
- inputs:{
+ inputs: {
'input.dropdown': DropdownInput,
'input.timerange': TimeRangeInput,
'input.text': TextInput,
diff --git a/template/src/timelapse/controls.js b/template/src/timelapse/controls.js
new file mode 100644
index 0000000..46b4e57
--- /dev/null
+++ b/template/src/timelapse/controls.js
@@ -0,0 +1,230 @@
+import React, { useEffect, useMemo, useCallback } from "react";
+import styled from "styled-components";
+import ColumnChart from "@splunk/react-visualizations/Column";
+import Play from "@splunk/react-icons/Play";
+import Pause from "@splunk/react-icons/Pause";
+import Spinner from "@splunk/react-ui/WaitSpinner";
+import Slider from "./slider";
+import {
+ globalTime,
+ useTimeList,
+ useCurrentTime,
+ usePlaybackStatus,
+ usePlaybackSpeed,
+} from "./timecontext";
+import { useState } from "react";
+
+const Wrapper = styled.div`
+ position: fixed;
+ tops: 0;
+ left: 0;
+ right: 0;
+ height: 125px;
+ background: #ffffff;
+ border-bottom: 5px solid rgb(8, 9, 10);
+ color: #444444;
+ z-index: 999;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+`;
+
+const LoadingCt = styled.div`
+ flex: 1;
+ display: flex;
+ justify-content: center;
+`;
+
+const PlaybackControls = styled.div`
+ width: 120px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+`;
+
+const CurrentValueCt = styled.div`
+ width: 190px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 25px;
+`;
+
+const Timeline = styled.div`
+ flex: 1;
+ position: relative;
+`;
+
+const SliderCt = styled.div`
+ box-sizing: border-box;
+ padding: 0 10px 0 10px;
+ background: rgba(255, 255, 255, 0.001);
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 120px;
+ z-index: 999;
+`;
+let i = 0;
+
+function minutes_with_leading_zeros(dt) {
+ return (dt.getMinutes() < 10 ? "0" : "") + dt.getMinutes();
+}
+
+function CurrentTimeValue() {
+ const val = useCurrentTime();
+ if (!val) {
+ return null;
+ }
+ return (
+
+ {val.getMonth() + 1}/{val.getDate()}/{val.getYear() % 100}{" "}
+ {val.getHours()}:{minutes_with_leading_zeros(val)}
+
+ );
+}
+
+const Btn = styled.a`
+ display: block;
+ color: #444444;
+ text-decoration: none;
+ padding: 10px;
+ cursor: pointer;
+`;
+
+function PlayPauseButton() {
+ const isPlaybackRunning = usePlaybackStatus();
+
+ const startPlayback = useCallback(() => {
+ globalTime.startPlayback();
+ }, []);
+
+ const stopPlayback = useCallback(() => {
+ globalTime.stopPlayback();
+ }, []);
+
+ return (
+
+ {isPlaybackRunning ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+function PlaybackSpeed() {
+ const curSpeed = usePlaybackSpeed();
+ const handleChange = useCallback((e) => {
+ globalTime.speed = +e.target.value;
+ }, []);
+
+ return (
+
+ );
+}
+
+export default function TimelapseControls({ definition }) {
+ const [values, setValues] = useState([]);
+ useEffect(() => {
+ let active = true;
+ (async () => {
+ const ds = Object.values(definition.dataSources).find(
+ (def) => def.name === "Time Series"
+ );
+ if (!ds) {
+ throw new Error("MISSING TIMELINE DATASOURCE");
+ }
+ const res = await fetch(ds.options.uri);
+ const data = await res.json();
+ if (active) {
+ const times = data.columns[data.fields.indexOf("_time")];
+ const caseNumbers = data.columns[data.fields.indexOf("x")];
+
+ const newValues = times
+ .sort()
+ .map((_, i) => i)
+ .map((v) => parseFloat(caseNumbers[v]))
+ .reduce(
+ (res, v, i, all) => {
+ return i >= all.length - 1
+ ? res
+ : res.concat(
+ Math.min(0.6, 0.05 + Math.max(0, Math.log2(all[i + 1] / v)))
+ );
+ },
+ [0.05]
+ );
+
+ globalTime.setTimes(times);
+ setValues(newValues);
+ globalTime.startPlayback();
+ }
+ })().catch((e) => {
+ console.error(e);
+ });
+
+ return () => {
+ active = false;
+ };
+ }, [definition, setValues]);
+
+ const times = useTimeList();
+
+ if (!times.length || !values.length) {
+ return (
+
+
+
+
+
+ );
+ }
+ return (
+
+
+
+ {/* */}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/template/src/timelapse/index.js b/template/src/timelapse/index.js
new file mode 100644
index 0000000..711c877
--- /dev/null
+++ b/template/src/timelapse/index.js
@@ -0,0 +1,45 @@
+import React, { useEffect } from "react";
+import styled from "styled-components";
+import TimelapseControls from "./controls";
+import Dashboard from "../components/dashboard";
+import DEFAULT_PRESET from "../preset";
+import TimelapseDataSource from "./timelapseds";
+import jsCharting from "@splunk/charting-bundle";
+
+function hackDisableProgressiveRender() {
+ const c = jsCharting.createChart(document.createElement("div"), {});
+ c.constructor.prototype.shouldProgressiveDraw = () => false;
+ c.destroy();
+}
+
+window.jsCharting = jsCharting;
+
+const DashboardWrapper = styled.div`
+ padding-top: 125px;
+`;
+
+const TIMELAPSE_PRESET = {
+ ...DEFAULT_PRESET,
+ dataSources: {
+ "ds.cdn": TimelapseDataSource,
+ },
+};
+
+export default function TimelapseDashboard({ dashDef }) {
+ useEffect(() => {
+ hackDisableProgressiveRender();
+ }, []);
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/template/src/timelapse/slider.js b/template/src/timelapse/slider.js
new file mode 100644
index 0000000..5465851
--- /dev/null
+++ b/template/src/timelapse/slider.js
@@ -0,0 +1,107 @@
+import React, { useState, useCallback, useEffect, useRef } from 'react';
+import styled from 'styled-components';
+import { globalTime } from './timecontext';
+import EventListener from 'react-event-listener';
+
+const Wrapper = styled.div`
+ width: 100%;
+ height: 120px;
+ background: rgba(200, 0, 0, 0.001);
+ cursor: pointer;
+ position: relative;
+`;
+
+const Thumb = styled.div`
+ height: 120px;
+ background: rgba(255, 255, 255, 0.001);
+ cursor: move;
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
+const ThumbMarker = styled.div`
+ height: 100px;
+ width: 1px;
+ border: 1px solid #d5e3d7;
+ background: #d5e3d7;
+ border-radius: 5px;
+
+ ${Thumb}:hover & {
+ border: 2px solid #d5e3d7;
+ }
+ [data-selected='true'] & {
+ border: 2px solid #d5e3d7;
+ }
+`;
+
+export default function Slider({ times }) {
+ const [dragging, setDragging] = useState(false);
+ const [val, setVal] = useState(0);
+ const [selected, setSelected] = useState(false);
+ const trackEl = useRef();
+
+ useEffect(
+ () =>
+ globalTime.subscribe(() => {
+ setVal(globalTime._cur);
+ }),
+ [setVal]
+ );
+
+ const handleClick = useCallback(
+ e => {
+ const boundingRect = trackEl.current.getBoundingClientRect();
+ const offset = e.clientX - boundingRect.left;
+ const value = Math.floor((offset / boundingRect.width) * times.length);
+ setVal(value + 0.5);
+ globalTime.stopPlayback();
+ globalTime.setTime(value + 0.5);
+ },
+ [setSelected, setVal, trackEl]
+ );
+
+ const handleDragStart = useCallback(
+ e => {
+ if (e.button > 0) {
+ return;
+ }
+ e.preventDefault();
+ setDragging(true);
+ },
+ [setDragging]
+ );
+
+ const handleDragEnd = useCallback(e => {
+ setDragging(false);
+ }, []);
+
+ const handleDrag = useCallback(
+ e => {
+ const boundingRect = trackEl.current.getBoundingClientRect();
+ const offset = e.clientX - boundingRect.left;
+ const value = Math.min(Math.floor((offset / boundingRect.width) * times.length), times.length - 1);
+ setVal(value + 0.5);
+ globalTime.stopPlayback();
+ globalTime.setTime(value + 0.5);
+ },
+ [dragging]
+ );
+
+ return (
+
+ {dragging && }
+
+
+
+
+ );
+}
diff --git a/template/src/timelapse/timecontext.js b/template/src/timelapse/timecontext.js
new file mode 100644
index 0000000..e898d2e
--- /dev/null
+++ b/template/src/timelapse/timecontext.js
@@ -0,0 +1,218 @@
+import { useState, useEffect } from 'react';
+
+class GlobalTime {
+ _times = [];
+ _subs = new Set();
+ _cur = null;
+ _timer = null;
+ _loop = true;
+ _speed = 10;
+ span = 43200000;
+
+ setRange(from, to) {
+ const times = [];
+ let start = from;
+ const now = to || new Date();
+ while (start < now) {
+ times.push(start.getTime());
+ start = new Date(start.getTime() + this.span);
+ }
+ this._times = times;
+ this.setTime(0);
+ }
+
+ setTimes(times) {
+ this._times = times.map(t => new Date(t).getTime());
+ this._times.sort();
+ this.setTime(0);
+ }
+
+ subscribe(callback) {
+ this._subs.add(callback);
+ if (this._cur != null) {
+ callback(this._cur);
+ }
+ return () => {
+ this._subs.delete(callback);
+ };
+ }
+
+ subscribeToTime(callback) {
+ let cur = this.currentTime;
+ if (cur != null) {
+ callback(cur);
+ }
+ return this.subscribe(() => {
+ const next = this.currentTime;
+ if (next?.getTime() != cur?.getTime()) {
+ callback(next);
+ cur = next;
+ }
+ });
+ }
+
+ subscribeToTime(callback) {
+ let cur = this.currentTime;
+ if (cur != null) {
+ callback(cur);
+ }
+ return this.subscribe(() => {
+ const next = this.currentTime;
+ if (next?.getTime() != cur?.getTime()) {
+ callback(next);
+ cur = next;
+ }
+ });
+ }
+
+ subscribeToTimeSpan(callback) {
+ let cur = this._cur;
+ if (cur != null) {
+ callback(this.timeSpanAt(cur));
+ }
+ return this.subscribe(() => {
+ const next = this._cur;
+ if (next != cur) {
+ callback(this.timeSpanAt(next));
+ cur = next;
+ }
+ });
+ }
+
+ get currentTime() {
+ const dateVal = this._times[Math.max(0, Math.min(this._times.length - 1, Math.floor(this._cur)))];
+ return dateVal != null ? new Date(dateVal) : null;
+ }
+
+ timeSpanAt(idx) {
+ if (idx != null) {
+ idx = Math.max(0, Math.min(this._times.length - 1, Math.floor(idx)));
+ const start = this._times[idx];
+ if (start != null) {
+ const endVal = this._times[idx + 1];
+ const end = endVal != null ? endVal : start + this.span;
+ return [start, end];
+ }
+ }
+ return null;
+ }
+
+ get currentTimeSpan() {
+ return this._cur != null ? this.timeSpanAt(this._cur) : null;
+ }
+
+ get current() {
+ return this._cur;
+ }
+
+ get timeList() {
+ return this._times;
+ }
+
+ get speed() {
+ return this._speed;
+ }
+
+ set speed(val) {
+ this._speed = val;
+ if (this.isPlaybackRunning()) {
+ this.stopPlayback();
+ this.startPlayback();
+ }
+ }
+
+ setTime(time) {
+ if (time !== this._cur) {
+ this._cur = time;
+ this.notify();
+ }
+ }
+
+ notify() {
+ const newVal = this.currentTime;
+ for (const sub of this._subs.values()) {
+ sub(newVal);
+ }
+ }
+
+ isPlaybackRunning() {
+ return this._timer != null;
+ }
+
+ startPlayback() {
+ this.stopPlayback();
+ this._timer = setInterval(() => {
+ if (this._cur < this._times.length) {
+ this.setTime(this._cur + 0.1);
+ } else {
+ if (this._loop) {
+ this.setTime(0);
+ } else {
+ this.stopPlayback();
+ }
+ }
+ }, Math.floor(64 * (1 / this._speed)));
+ }
+
+ stopPlayback() {
+ if (this._timer != null) {
+ this._timer = clearInterval(this._timer);
+ this._timer = null;
+ this.notify();
+ }
+ }
+}
+
+export const globalTime = new GlobalTime();
+
+export function useCurrentTime() {
+ const [val, setVal] = useState(globalTime.currentTime);
+
+ useEffect(() => {
+ const unsubscribe = globalTime.subscribeToTime(val => setVal(val));
+ return () => {
+ unsubscribe();
+ };
+ }, [setVal]);
+
+ return val;
+}
+
+export function useTimeList() {
+ const [val, setVal] = useState(globalTime.timeList);
+ useEffect(() => {
+ const unsubscribe = globalTime.subscribe(() => {
+ setVal(globalTime.timeList);
+ });
+ return () => {
+ unsubscribe();
+ };
+ }, [setVal]);
+ return val;
+}
+
+export function usePlaybackStatus() {
+ const [val, setVal] = useState(globalTime.timeList);
+ useEffect(() => {
+ const unsubscribe = globalTime.subscribe(() => {
+ setVal(globalTime.isPlaybackRunning());
+ });
+ return () => {
+ unsubscribe();
+ };
+ }, [setVal]);
+ return val;
+}
+
+export function usePlaybackSpeed() {
+ const [val, setVal] = useState(globalTime.speed);
+ useEffect(() => {
+ const unsubscribe = globalTime.subscribe(() => {
+ setVal(globalTime.speed);
+ });
+ return () => {
+ unsubscribe();
+ };
+ }, [setVal]);
+ return val;
+}
diff --git a/template/src/timelapse/timelapseds.js b/template/src/timelapse/timelapseds.js
new file mode 100644
index 0000000..1f5b54e
--- /dev/null
+++ b/template/src/timelapse/timelapseds.js
@@ -0,0 +1,130 @@
+import DataSource from "@splunk/datasources/DataSource";
+import DataSet from "@splunk/datasource-utils/DataSet";
+import { globalTime } from "./timecontext";
+import { createDataSet } from "../datasource";
+import { registerScreenshotReadinessDep } from "../ready";
+
+function capAt(fields, columns, timeColIdx, untilRow) {
+ return DataSet.fromJSONCols(
+ fields,
+ columns.map((c) => c.slice(0, untilRow))
+ );
+}
+
+function nullAfter(fields, columns, timeColIdx, untilRow) {
+ return DataSet.fromJSONCols(
+ fields,
+ columns.map((c, i) => {
+ if (i === timeColIdx) {
+ return c;
+ }
+ return c.map((v, r) => (r >= untilRow ? null : v));
+ })
+ );
+}
+
+export default class TimelapseDataSource extends DataSource {
+ constructor(options = {}, context = {}) {
+ super(options, context);
+ this.uri = options.uri;
+ }
+
+ request(options) {
+ options = options || {};
+ return (observer) => {
+ const onabortCallbacks = [];
+ let readyDep = registerScreenshotReadinessDep("TLDS");
+
+ fetch(this.uri)
+ .then((r) => r.json())
+ .then(({ fields, columns, timelapse }) => {
+ if (!timelapse) {
+ readyDep.ready();
+ observer.next({
+ data: createDataSet({ fields, columns }, options),
+ meta: {},
+ });
+ } else {
+ const timeFieldIdx = fields.indexOf(timelapse.field || "_time");
+ const parsedTimes = columns[timeFieldIdx].map((v) =>
+ new Date(v).getTime()
+ );
+
+ switch (timelapse.transform) {
+ case "null_after":
+ case "cap":
+ console.log("Found a cap");
+
+ {
+ const updateUntilTime = ([time]) => {
+ const t = time;
+ let untilRow = parsedTimes.findIndex((v) => v > t);
+ if (untilRow < 0) {
+ untilRow = Infinity;
+ }
+ const fn =
+ timelapse.transform === "cap" ? capAt : nullAfter;
+ observer.next({
+ data: fn(fields, columns, timeFieldIdx, untilRow),
+ meta: { status: "done" },
+ });
+ readyDep.ready();
+ };
+ onabortCallbacks.push(
+ globalTime.subscribeToTimeSpan(updateUntilTime)
+ );
+ }
+ break;
+ case "select":
+ console.log("Found a select");
+ {
+ const selectNext = ([start, end]) => {
+ observer.next({
+ data: DataSet.fromJSONCols(
+ fields.filter((_, i) => i !== timeFieldIdx),
+ columns
+ .filter((_, i) => i !== timeFieldIdx)
+ .map((c) =>
+ c.filter((_, i) => {
+ const t = parsedTimes[i];
+ return t >= start && t < end;
+ })
+ )
+ ),
+ meta: { status: "done" },
+ });
+ readyDep.ready();
+ };
+ onabortCallbacks.push(
+ globalTime.subscribeToTimeSpan(selectNext)
+ );
+ }
+ break;
+ default:
+ throw new Error(
+ `Invalid timelapse transform ${timelapse.transform}`
+ );
+ }
+ }
+ })
+ .catch((e) => {
+ console.error(e);
+ observer.error({
+ level: "error",
+ message: e.message || "Unexpected error",
+ });
+ });
+
+ return () => {
+ for (const cb of onabortCallbacks) {
+ try {
+ cb();
+ } catch (e) {
+ console.error("Abort callback failed", e);
+ }
+ }
+ readyDep.remove();
+ };
+ };
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 66699b6..c4ad1d8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -195,10 +195,10 @@ chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
-chalk@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
- integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
+chalk@^4.1.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
@@ -287,6 +287,11 @@ cli-width@^2.0.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
+cli-width@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
+ integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
+
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
@@ -631,21 +636,21 @@ inquirer@^5.1.0:
strip-ansi "^4.0.0"
through "^2.3.6"
-inquirer@^7.1.0:
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29"
- integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==
+inquirer@^7.3.3:
+ version "7.3.3"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
+ integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
dependencies:
ansi-escapes "^4.2.1"
- chalk "^3.0.0"
+ chalk "^4.1.0"
cli-cursor "^3.1.0"
- cli-width "^2.0.0"
+ cli-width "^3.0.0"
external-editor "^3.0.3"
figures "^3.0.0"
- lodash "^4.17.15"
+ lodash "^4.17.19"
mute-stream "0.0.8"
run-async "^2.4.0"
- rxjs "^6.5.3"
+ rxjs "^6.6.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
through "^2.3.6"
@@ -741,11 +746,16 @@ lodash.templatesettings@^4.0.0:
dependencies:
lodash._reinterpolate "^3.0.0"
-lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.5, lodash@^4.3.0:
+lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+lodash@^4.17.19:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -937,10 +947,10 @@ prebuild-install@^6.1.4:
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
-prettier@^1.19.1:
- version "1.19.1"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
- integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
+prettier@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.0.tgz#e7b19f691245a21d618c68bc54dc06122f6105ae"
+ integrity sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==
process-nextick-args@~2.0.0:
version "2.0.1"
@@ -1036,10 +1046,10 @@ rxjs@^5.5.2:
dependencies:
symbol-observable "1.0.1"
-rxjs@^6.5.3:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
- integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
+rxjs@^6.6.0:
+ version "6.6.7"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+ integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
dependencies:
tslib "^1.9.0"