diff --git a/LICENSE b/LICENSE index d046d22..bf3e3af 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,13 @@ MIT License -Copyright (c) 2021 DriveWorks +Copyright (C) 2020 DriveWorks Ltd -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this example and associated documentation files (the "Example"), to deal in the Example without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Example, and to permit persons to whom the Example is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Example. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE EXAMPLE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE EXAMPLE OR THE USE OR OTHER DEALINGS IN THE EXAMPLE. + +Any third party software used is covered under their own license agreements. + +GeoJS (https://www.geojs.io/) https://github.com/jloh/geojs/blob/master/LICENCE diff --git a/README.md b/README.md index 4cc9c61..c882f76 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ -# IntegrationThemeExample-Dashboard -A web dashboard combining data sources from the Integration Theme Web API & Live Licensing endpoints +# DriveWorks Live - Integration Theme Example - Dashboard +### Version: 1.4.6 +#### Minimum DriveWorks Version: 18.0 + +An example web dashboard combining data sources from the DriveWorks Integration Theme Web API & Live License Server endpoints. + +Please note: DriveWorks are not accepting pull requests for this example. +Join our [online community](https://my.driveworks.co.uk) for discussion, resources and to suggest other examples. + +### Features + +- **License Session Data** + - Centralized, OnDemand and Total Entitlement + - Current Connections + - Current Usage + - Peak Usage + +- **Group Data** + - Projects + - Specifications (Generated and Queued) + - Documents (Generated and Queued) + +- **Live Sessions** + - Server Name + - Country of Origin [optional] + - Unique Address + - Session Length + - Browser + +### To use: +1. Clone this repository, or download as a .zip + +2. Enter your Integration Theme details into `config.js` + * `serverUrl` - The URL that hosts your Integration Theme, including any ports. + * `groupAlias` - The public alias created for the Group containing the data to render - as configured in DriveWorksConfigUser.xml. + * This connection is used to access API data, such as Project/Specification/Document counts. + * See [Integration Theme Settings](https://docs.driveworkspro.com/Topic/IntegrationThemeSettings) for additional guidance. + * `credentials` - [optional] The username and password of the Group user to login. + * For publicly hosted content, it is advised that default credentials are instead stored within the XML config file (DriveWorksConfigUser.xml). + * See [Integration Theme Connection Settings](https://docs.driveworkspro.com/Topic/IntegrationThemeSettings#Connection-Settings) for additional guidance. + * Credentials could also be collected via a login form on-demand. See the related [Simple Login Example](https://github.com/DriveWorks/IntegrationThemeExample-SimpleLogin) for additional guidance. + * `licenseDataUrl` - Address and port of a valid DriveWorks Live Floating License Server, from which to access usage data. + * Example: http://YOUR-LICENCE-SERVER:27080 + * See [DriveWorks Live Licensing API Help](https://docs.driveworkspro.com/topic/LicenseManagerDriveWorksLive#driveworks-live-licensing-api) for additional guidance. + * `driveWorksMajorVersion` - Major version of DriveWorks to display data from (multiple may be available) e.g. '18' + +3. [optional] Configure additional settings + * `statusRefreshInterval` - The interval (in seconds) to request status data (version major/minor) + * `infoRefreshInterval` - The interval (in seconds) to request license info data (max sessions, on-demand cap, peak usage etc.) + * `sessionRefreshInterval` - The interval (in seconds) to request live session data (server name, hostname/address, browser, start time etc.) + * `apiRefreshInterval` - The interval (in seconds) to request DriveWorks Live API data (Projects/Specifications/Documents) + * `dateLocale` - ISO 639-1 standard language code (e.g. en-US). Used to format dates displayed. + * `defaultTheme` - (light/dark) Color theme for dashboard. + * `ipAddressLookup` - (true/false) Map session user address to country of origin. + * Uses [GeoJS](https://github.com/jloh/geojs) for user address lookup - please refer to it's [Third Party Licensing](https://github.com/jloh/geojs/blob/master/LICENCE) + * `knownAddresses` - Provide a list of known addresses to skip address lookup and provide custom location identification. + * Example: 1.1.1.1 - "HQ", "Main Office" + * See commented format. + +4. In `index.html`, replace "YOUR-DRIVEWORKS-LIVE-SERVER-URL" with the URL of your own DriveWorks Live server that is serving `DriveWorksLiveIntegrationClient.min.js` - including any ports. + * This should be the URL that hosts the Integration Theme, and serves it's landing page. + * To check that this URL is correct, attempt to load DriveWorksLiveIntegrationClient.min.js in a browser. It should return a minified code library. + +5. Host the example locally or on a remote server. + * Ensure `` in DriveWorksConfigUser.xml permits request from this location. + See [Integration Theme Settings](https://docs.driveworkspro.com/Topic/IntegrationThemeSettings) for additional guidance. + +6. If encountering any issues, check the browser's console for error messages (F12) + +### Potential Issues: +* When serving this example for a domain different to your DriveWorks Live server, e.g. api.my-site.com from company.com, 'SameSite' cookie warnings may be thrown when the Client SDK attempts to store the current session id. + * This appears as "Error: 401 Unauthorized" in the browser console, even with the correct configuration set. + * To resolve: + * Ensure you are running DriveWorks 18.2 or above, HTTPS is enabled in DriveWorks Live's settings and a valid SSL certificate has been configured via DriveWorksConfigUser.xml. + * See [Integration Theme Settings](https://docs.driveworkspro.com/Topic/IntegrationThemeSettings) for additional guidance. + +--- + +This source code has been made available to demonstrate how you can integrate with DriveWorks using the DriveWorks Live API. +This code is provided under the MIT license, for more details see LICENSE.md. + +The example requires that you have the latest DriveWorks Live SDK installed, operational and remotely accessible. diff --git a/config.js b/config.js new file mode 100644 index 0000000..8aa2cf9 --- /dev/null +++ b/config.js @@ -0,0 +1,34 @@ +// Update these values to match those of your Server URL, DriveWorks Group Alias (optional: Credentials) +// and Licensing Data URL + +// You can also enter any known IP addresses to: +// - Skip the IP lookup for these addresses +// - Override the country information displayed +// - Example: you could mark your internal IP as "Internal"/"INT", to distinguish internal traffic. + +const config = { + serverUrl: "", + groupAlias: "", + credentials: { + username: "", + password: "", + }, + licenseDataUrl: "", // Example: http://YOUR-LICENCE-SERVER:27080 - See https://docs.driveworkspro.com/topic/LicenseManagerDriveWorksLive#driveworks-live-licensing-api + driveWorksMajorVersion: 18, // Major version of DriveWorks to display data from (multiple may be available) + statusRefreshInterval: 120, // seconds + infoRefreshInterval: 10, // seconds + sessionRefreshInterval: 10, // seconds + apiRefreshInterval: 30, // seconds + dateLocale: "en-US", // ISO 639-1 standard language code (e.g. en-US) + defaultTheme: "light", // light/dark + ipAddressLookup: false, // Enable reverse lookup for session IP country of origin + knownAddresses: [ // List of known IP addresses, to override IP address lookup e.g. Main Office IP Address = "HQ" + // [ + // "123.123.123.123", + // { + // "country": "Location Name", // Written name for the location e.g. "United States", "United Kingdom" + // "countryCode": "CO", // The short-code for the location e.g. "US", "GB" (ISO 3166-1 alpha-2) + // } + // ], + ], +}; diff --git a/css/dashboard.css b/css/dashboard.css new file mode 100644 index 0000000..039413c --- /dev/null +++ b/css/dashboard.css @@ -0,0 +1,575 @@ +:root { + + --status-default: #4299e1; + --status-ok: #48bb78; + --status-warning: #ed8936; + --status-error: #e53e3e; + + /* Light Theme */ + --bg-color: #eee; + --overlay-bg: 0, 0, 0; + --bg-color-rgb: 238, 238, 238; + --text-color: #888; + --accent-light: #fff; + --accent-dark: #eee; + + /* Dark Theme */ + --dark-bg-color: #29292e; + --dark-bg-color-rgb: 41, 41, 46; + --dark-text-color: rgba(255,255,255,.75); + --dark-accent-light: #343439; + --dark-accent-dark: #2c2c31; +} + +/* Theme: Dark */ + +.theme-dark { + --bg-color: var(--dark-bg-color); + --bg-color-rgb: var(--dark-bg-color-rgb); + --text-color: var(--dark-text-color); + --accent-light: var(--dark-accent-light); + --accent-dark: var(--dark-accent-dark); +} + +@media (prefers-color-scheme: dark) { + + :root { + --bg-color: var(--dark-bg-color); + --bg-color-rgb: var(--dark-bg-color-rgb); + --text-color: var(--dark-text-color); + --accent-light: var(--dark-accent-light); + --accent-dark: var(--dark-accent-dark); + } + +} + + +/* Base */ + +html { + box-sizing: border-box; +} +*, +*::before, +*::after { + box-sizing: inherit; +} + +html, +body { + width: 100%; + height: 100%; + margin: 0; +} + +body { + font-family: 'Open Sans', sans-serif; + background-color: var(--bg-color); + color: var(--text-color); +} + +button, +.button { + display: inline-block; + font-family: inherit; + font-size: 1em; + font-weight: 600; + padding: 1em 2em; + border: none; + border-radius: 100em; + text-align: center; + cursor: pointer; + background-color: var(--accent-dark); + color: var(--text-color); +} +button:hover, +.button:hover { + filter: brightness(105%); +} +button:focus, +.button:focus { + outline: none; + box-shadow: 0 0 0 2px #4299e1; +} + + +/* Structure */ + +.dashboard-wrapper { + display: flex; + flex-direction: column; + width: 100%; + min-height: 100%; +} + + +/* Header */ + +.dashboard-header { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + width: 100%; + padding: 1em 1.5em; +} + +@media screen and (min-width: 40em) { + .dashboard-header { + flex-wrap: nowrap; + padding: 1em 1.5em 0; + } + +} + +.dashboard-header > * { + margin: .25em 0; +} + +.page-title { + display: flex; + align-items: center; + width: 100%; + font-size: 1.5em; + font-weight: 400; + line-height: 1.1; + color: var(--text-color); +} + +@media screen and (min-width: 40em) { + .page-title { + font-size: 2em; + margin-right: 1em; + } + +} + +.page-title svg { + width: 1.5em; + min-width: 1.5em; + margin-right: .5em; +} +.page-title span { + opacity: .75; +} + +.version-number { + white-space: nowrap; + font-size: 1.2em; +} +.version-number span { + font-weight: 600; +} + +@media screen and (min-width: 40em) { + .version-number { + text-align: right; + } + +} + +@media screen and (min-width: 60em) { + .version-number { + font-size: 1.5em; + } + +} + +.source-url { + font-size: .75em; + opacity: .75; + margin-bottom: .25em; +} + + +/* Content */ + +.dashboard-content { + display: flex; + flex-wrap: wrap; + flex: 1; + height: 100%; + max-height: 100%; +} + +@media screen and (min-width: 60em) { + + .dashboard-content { + padding: 0 .5em .5em; + } + +} + +@media screen and (min-width: 80em) { + + .dashboard-content { + flex-wrap: nowrap; + } + +} + +.section-card { + width: 100%; + background: var(--accent-light); +} + +@media screen and (min-width: 40em) { + + .section-card { + margin: 1em; + border-radius: 1.5em; + overflow: hidden; + box-shadow: 0 0 20px rgba(0,0,0,.1); + } + +} + +@media screen and (min-width: 80em) { + + .section-card { + width: 50%; + } + +} + + +/* Data Card */ + +.data-card { + display: flex; + flex-direction: column; + position: relative; + padding: 2em 1em 1em; +} + +.stat-charts { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.chart-block { + width: 50%; + padding: 0 .5em; + margin-bottom: 1.5em; +} +.chart-block .title { + font-size: 1.2em; + line-height: 1.2; + text-align: center; +} + +@media screen and (min-width: 40em) { + + .chart-block { + width: 33%; + padding: 0 1em; + } + + .chart-block .title { + font-size: 1.5em; + } + +} + +.donut-chart { + position: relative; + width: 100%; + margin-bottom: .5em; +} + +.donut-chart .center { + fill: var(--accent-light); +} +.donut-chart .ring { + fill: var(--accent-dark); +} +.donut-chart .peak { + fill: transparent; + stroke: #000; + stroke-opacity: .15; +} +.donut-chart .progress { + fill: transparent; +} +.donut-chart .progress.status-ok { + stroke: var(--status-ok); +} +.donut-chart .progress.status-warning { + stroke: var(--status-warning); +} +.donut-chart .progress.status-error { + stroke: var(--status-error); +} + +.chart-value { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + font-size: 2.5em; + font-weight: 600; + text-align: center; + color: var(--text-color); + line-height: 1.15; + margin-top: -0.1em; +} +.chart-value .total { + font-size: .4em; + font-weight: 400; + opacity: .6; +} + +@media screen and (min-width: 60em) { + + .chart-value { + font-size: 3em; + } + +} + +@media screen and (min-width: 120em) { + + .chart-value { + font-size: 4em; + } + +} + +.divider { + padding: 0 em; +} +.divider hr { + border: 1px solid var(--accent-dark); + margin: .5em 0 2em; +} + +@media screen and (min-width: 40em) { + + .divider { + padding: 0 2em; + } + .divider hr { + margin: .5em 0 2em; + } + +} + +@media screen and (min-width: 100em) { + + .divider hr { + margin-bottom: 3em; + } + +} + +.stat-grid { + display: flex; + flex-wrap: wrap; + justify-content: center; + text-align: center; +} +.stat-grid .stat-item { + width: 50%; + padding: 0 .5em; + margin-bottom: 1.5em; + color: var(--font-color); +} +.stat-item .value { + font-size: 1.5em; + font-weight: 600; +} +.stat-item .value.value-small { + font-size: 1.5em; + font-weight: 600; + line-height: 1; + margin-top: .4em; +} +.stat-item .subtitle { + font-size: .85em; + font-weight: 400; + margin-top: .1em; + opacity: .75; +} + +@media screen and (min-width: 40em) { + + .stat-grid { + font-size: 1.25em; + } + + .stat-grid .stat-item { + width: 50%; + padding: 0 1em; + } + + .stat-item > * { + font-size: 2.5em; + } + +} + +@media screen and (min-width: 60em) { + + .stat-grid .stat-item { + width: 33%; + } + +} + +@media screen and (min-width: 100em) { + + .stat-grid { + font-size: 1.3em; + } + + .stat-grid .stat-item { + margin-bottom: 3em; + } + + .stat-item .value { + font-size: 2em; + } + +} + + +/* Sessions Table */ + +.sessions-card { + overflow: auto; +} + +.sessions-card::-webkit-scrollbar { + width: 1.25em; + height: 1.25em; +} +.sessions-card::-webkit-scrollbar-track { + background: var(--accent-dark); + border-radius: 0 1em 1em 0; +} +.sessions-card::-webkit-scrollbar-thumb { + border-radius: 10em; + background: var(--text-color); + border: 6px solid var(--accent-dark); +} +.sessions-card::-webkit-scrollbar-thumb:hover { + filter: brightness(80%); +} + +.sessions-table { + width: 100%; + background: var(--accent-dark); + text-align: left; +} + +.sessions-table table { + width: 100%; + border: none; + border-collapse: collapse; +} + +.sessions-table th { + background: var(--accent-light); + white-space: nowrap; + padding: 1.25em 1em; +} + +.sessions-table td { + padding: 1em; +} + +.sessions-table tr th:first-child, +.sessions-table tr td:first-child { + padding-left: 1.5em; +} + +.sessions-table tbody tr { + background: var(--accent-dark); +} +.sessions-table tbody tr:nth-child(even) { + background: var(--accent-light); +} + +.sessions-table .time-difference { + display: inline-block; + white-space: nowrap; +} + +.sessions-empty { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + text-align: center; + padding: 4em 3em; +} + +.sessions-empty .inner { + font-size: 2em; +} +.sessions-empty .inner svg { + display: block; + width: 3em; + height: auto; + margin: 0 auto 1em; + opacity: .2; +} + +.sessions-table.is-hidden, +.sessions-empty.is-hidden { + display: none; +} + +@media screen and (min-width: 40em) { + + .sessions-table td { + padding: 1em; + } + +} + +@media screen and (min-width: 60em) { + + .sessions-table th { + position: -webkit-sticky; + position: sticky; + top: 0; + box-shadow: 0 3px rgba(0,0,0,.05); + white-space: normal; + } + .browser-edge .sessions-table th { + position: static; + } + + .sessions-table tbody tr:hover { + background-color: var(--status-default); + color: #fff; + } + + .sessions-table tr th:first-child, + .sessions-table tr td:first-child { + padding-left: 1.5em; + } + +} + +@media screen and (max-width: 80em) { + + .sessions-card { + max-height: none !important; + } + + .session-hostname > span { + display: block; + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + +} diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..066b7b1 Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..cc07b63 --- /dev/null +++ b/index.html @@ -0,0 +1,193 @@ + + + + + + + + + + Dashboard | DriveWorks + + + + + + + + + + +
+ +
+ +

+ + + + + + + Live Dashboard +

+ +
+ +
+ Source: +
+ + DriveWorks Version: --- + +
+ +
+ +
+ + +
+ + +
+ +
+
+ + + + +
+
+
Centralized Sessions
+
+ +
+
+ + + + +
+
+
OnDemand Sessions
+
+ +
+
+ + + + +
+
+
Total Sessions
+
+ +
+ +
+
+
+ + +
+ +
+ Centralized Entitlement +
---
+
+ +
+ OnDemand Entitlement +
---
+
+ +
+ Total Entitlement +
---
+
+ +
+ Peak Sessions +
---
+
+ +
+ Newest Session +
---
+
+ +
+ Oldest Session +
---
+
+ +
+ Projects +
---
+
+ +
+ Specifications +
---
+
---
+
+ +
+ Documents +
---
+
---
+
+ +
+ +
+ + +
+ + + +
+
+ + + + No active sessions +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/core.js b/js/core.js new file mode 100644 index 0000000..eb66815 --- /dev/null +++ b/js/core.js @@ -0,0 +1,57 @@ +/** + * Theme Switching + */ + +// By config (default) +const defaultTheme = config.defaultTheme; +if (defaultTheme){ + document.body.classList.add(`theme-${defaultTheme}`); +} + +// By url query +const urlQuery = new URLSearchParams(window.location.search); +const queryTheme = urlQuery.get("theme"); +if (queryTheme){ + + // Remove any previously set theme classes + document.body.classList.forEach(className => { + if (className.startsWith("theme-")) { + document.body.classList.remove(className); + } + }); + + // Add specified theme class + document.body.classList.add(`theme-${queryTheme}`); + +} + +/** + * Extract configured DriveWorks version data from set + */ +function getDriveWorksVersionIndex(data){ + return (data.driveWorksMajorVersion || data.majorVersion) === config.driveWorksMajorVersion; +} + +/** + * Basic browser detection + */ +const currentBrowser = (function (agent) { + switch (true) { + case agent.indexOf("edge") > -1: + return "edge"; + case agent.indexOf("edg") > -1: + return "edge-chromium"; + case agent.indexOf("opr") > -1 && !!window.opr: + return "opera"; + case agent.indexOf("chrome") > -1 && !!window.chrome: + return "chrome"; + case agent.indexOf("firefox") > -1: + return "firefox"; + case agent.indexOf("safari") > -1: + return "safari"; + default: return "other"; + } +})(window.navigator.userAgent.toLowerCase()); + +// Add current browser as class to body +document.body.classList.add(`browser-${currentBrowser}`); diff --git a/js/info.js b/js/info.js new file mode 100644 index 0000000..87e9e3a --- /dev/null +++ b/js/info.js @@ -0,0 +1,107 @@ +// Run on load +(async function() { + + getInfo(); + +})(); + +async function getInfo() { + + const info = await fetch(config.licenseDataUrl + "/info"); + const data = await info.json(); + renderInfo(data); + + // Refresh + setTimeout(getInfo, config.infoRefreshInterval * 1000); + +} + +function renderInfo(data) { + + // Parse Response + const index = data.findIndex(getDriveWorksVersionIndex); + + const maxConcurrentUsers = data[index].maxConcurrentUsers; + const activeSessions = data[index].activeSessionsCount; + const onDemandCap = data[index].onDemandCap; + const peakSessions = data[index].sessionsPeak; + + // Calculate total users + let totalUsers = 0; + let totalDisplay = 0; + + if (maxConcurrentUsers !== 0) { + totalUsers = maxConcurrentUsers + onDemandCap; + } + + if (totalUsers === 0) { + totalUsers = 1; + } else { + totalDisplay = totalUsers; + } + + // Calculate centralised users + let centralizedDisplay = activeSessions; + if (activeSessions >= maxConcurrentUsers) { + centralizedDisplay = maxConcurrentUsers; + } + + // OnDemand sessions + let onDemandDisplay = 0; + if (activeSessions >= maxConcurrentUsers) { + onDemandDisplay = activeSessions - maxConcurrentUsers; + } + + // Calculate percentages + centralizedPercentage = centralizedDisplay === 0 ? 0 : Math.round((centralizedDisplay / maxConcurrentUsers) * 100); + onDemandPercentage = onDemandDisplay === 0 ? 0 : Math.round((onDemandDisplay / onDemandCap) * 100); + activePercentage = activeSessions === 0 ? 0 : Math.round((activeSessions / totalDisplay) * 100); + peakPercentage = peakSessions === 0 ? 0 : Math.round((peakSessions / totalDisplay) * 100); + + // Create charts + drawDonutChart("centralized-sessions-chart", centralizedDisplay, centralizedPercentage, maxConcurrentUsers); + drawDonutChart("ondemand-sessions-chart", onDemandDisplay, onDemandPercentage, onDemandCap); + drawDonutChart("active-sessions-chart", activeSessions, activePercentage, totalDisplay, peakPercentage); + + // Render info + document.getElementById("total-users-limit").innerHTML = maxConcurrentUsers; + document.getElementById("ondemand-sessions-limit").innerHTML = onDemandCap; + document.getElementById("total-sessions-limit").innerHTML = totalDisplay; + document.getElementById("peak-sessions-total").innerHTML = peakSessions; + +} + +// Create Donut Charts +function drawDonutChart(el, value, percentage, total, peak) { + + // Options + const donutWidth = 2.5; + + // Ensure minumum percentage is 1 (to show something on the chart) + if (value > 0 && percentage === 0){ + percentage++; + } + + // Set status + let status = "ok"; + if (percentage >= 75){ + status = "warning"; + } + + // Draw chart + document.getElementById(el).innerHTML = ` + + + ${peak ? `` : ""} + + + +
+
+ ${value} +
/ ${total}
+
+
+ `; + +} diff --git a/js/sessions.js b/js/sessions.js new file mode 100644 index 0000000..3e37083 --- /dev/null +++ b/js/sessions.js @@ -0,0 +1,253 @@ +const dataCard = document.getElementById("data-card"); +const sessionsCard = document.getElementById("sessions-card"); +const sessionsTable = document.getElementById("sessions-table"); +const sessionsEmpty = document.getElementById("sessions-empty"); + +const knownAddresses = new Map(config.knownAddresses); +let storedData = []; +let serverTimeOffset; + +// Run on document ready +(async function () { + + sizeSessionsCard(); + + serverTimeOffset = await calculateServerTimeOffset(); + getSessions(); + +})(); + +// On page load +window.onload = function () { + sizeSessionsCard() +}; + +// On resize +window.addEventListener("resize", function () { + sizeSessionsCard(); +}); + +// Calculate sessions table card max-height +async function sizeSessionsCard() { + + // Remove an height affecting calculation + sessionsCard.style.maxHeight = "0"; + + // Set max-height to height of data card + const dataHeight = dataCard.offsetHeight; + sessionsCard.style.maxHeight = `${Math.floor(dataHeight - 1)}px`; + +} + +// Get session data +async function getSessions() { + + try { + + // Get data + const response = await fetch(config.licenseDataUrl + "/sessions"); + const data = await response.json(); + + // Extract data for specified version + const index = data.findIndex(getDriveWorksVersionIndex); + const sessions = data[index].sessions; + if (sessions.length) { + + // Sort by desc date order (newest first) + let sortedSessions = JSON.parse(JSON.stringify(sessions)); + sortedSessions = sortedSessions.sort((a, b) => new Date(b.sessionStarted) - new Date(a.sessionStarted)); + + // Update session table (if: empty (page load) OR open + data has changed) + if (storedData.length === 0 || JSON.stringify(data) !== JSON.stringify(storedData)) { + getSessionLocations(sortedSessions); + } + + // Store data for later comparison + storedData = data; + + // Update session durations + calculateSessionLengths(sortedSessions); + + } else { + // Show empty state (if previously hidden) + sessionsTable.classList.add("is-hidden"); + sessionsEmpty.classList.remove("is-hidden"); + } + + } catch (error) { + console.log(error); + } + + // Refresh on interval + setTimeout(getSessions, config.sessionRefreshInterval * 1000); + +} + +// Get locations from session IP +async function getSessionLocations(sessions) { + + // Extract IP addresses from hostname. Lookup if not in known addresses (new IP) + const newIpAddresses = []; + for (const [index, session] of sessions.entries()) { + + const ip = extractIpAddress(session.hostName); + sessions[index].ipAddress = ip; + + // If an IP address is found in the hostname, and has not been previously mapped, store for lookup + if (ip && !knownAddresses.has(ip) && newIpAddresses.indexOf(ip) === -1) { + newIpAddresses.push(ip); + } + + } + + // Lookup geolocation of new, unknown IP addresses + // Source: GeoJS (https://www.geojs.io/) + // MIT License: https://github.com/jloh/geojs/blob/master/LICENCE (subject to change by external parties) + if (config.ipAddressLookup && newIpAddresses.length) { + + try { + + const response = await fetch(`https://get.geojs.io/v1/ip/country.json?ip=${newIpAddresses.join()}`); + if (response.ok) { + const locations = await response.json(); + + // For each new address, check a location is returned (by index) + for (const ipAddress of newIpAddresses) { + const addressIndex = locations.map((location) => { return location.ip; }).indexOf(ipAddress); + if (addressIndex > -1) { + + // Extract geolocation information required, with clear key names + const location = locations[addressIndex]; + if (location.name && location.country) { + const countryDetails = { + "country": location.name, + "countryCode": location.country, + }; + + knownAddresses.set(ipAddress, countryDetails); + } + } + } + + } + + } catch (error) { + console.log(error); + } + } + + renderSessions(sessions); + +} + +// Render sessions to table +function renderSessions(sessions) { + + // Show table + sessionsEmpty.classList.add("is-hidden"); + sessionsTable.classList.remove("is-hidden"); + + // Generate session details + let sessionsMarkup = ""; + for (let i = 0; i < sessions.length; i++) { + + // Calculate start date + const options = { + dateStyle: "short", + timeStyle: "short", + }; + const sessionStarted = new Date(sessions[i].sessionStarted); + const sessionStartedDate = sessionStarted.toLocaleString(config.dateLocale, options); + + // Get location + const sessionAddress = knownAddresses.get(sessions[i].ipAddress); + const countryName = sessionAddress && sessionAddress.country ? sessionAddress.country : "No location available."; + const countryCode = sessionAddress && sessionAddress.countryCode ? sessionAddress.countryCode : "---"; + + // Create markup + sessionsMarkup += ` + + ${sessions[i].serverName.toLowerCase()} + ${countryCode} + ${sessions[i].hostName.split(":")[0]} + ${sessionStartedDate} (${timeDifference(sessionStarted)}) + ${sessions[i].browser.split(".")[0]} + + `; + + } + + // Display Data + document.querySelector("#sessions-table tbody").innerHTML = sessionsMarkup; + +} + +// Calculate the difference between two dates (in a human readable format) +function timeDifference(previousDate) { + + const elapsedTime = (new Date() - previousDate) + serverTimeOffset; + const msPerMinute = 60 * 1000; + const msPerHour = msPerMinute * 60; + + if (elapsedTime < msPerMinute) { + const seconds = Math.round(elapsedTime / 1000); + const suffix = seconds > 1 ? "secs" : "sec"; + return `${seconds} ${suffix} ago`; + } + + else if (elapsedTime < msPerHour) { + const minutes = Math.round(elapsedTime / msPerMinute); + const suffix = minutes > 1 ? "mins" : "min"; + return `${minutes} ${suffix} ago`; + } + + const hours = Math.round(elapsedTime / msPerHour); + const suffix = hours > 1 ? "hrs" : "hr"; + return `${hours} ${suffix} ago`; + +} + +// Calculate newest/oldest session +function calculateSessionLengths(sessions) { + + // Newest session + const newestSession = new Date(sessions[0].sessionStarted); + document.getElementById("newest-session").innerHTML = timeDifference(newestSession); + + // Oldest session + const oldestSession = new Date(sessions[sessions.length - 1].sessionStarted); + document.getElementById("oldest-session").innerHTML = timeDifference(oldestSession); + +} + +// Extract IP Address from string +function extractIpAddress(host) { + const format = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/; + const check = host.match(format); + if (check) { + return check[0]; + } +} + +// Calculate offset between server's reported time and local device time +async function calculateServerTimeOffset() { + + try { + + const response = await fetch(config.licenseDataUrl); + if (response.ok) { + + // Extract server time from the end of the license version string + const data = await response.text(); + const serverTime = data.slice(-28); + + // Return the difference between server and current time + return new Date(serverTime) - new Date(); + } + + } catch (error) { + console.log(error); + return 0; + } + +} diff --git a/js/status.js b/js/status.js new file mode 100644 index 0000000..b18a215 --- /dev/null +++ b/js/status.js @@ -0,0 +1,43 @@ +// Run on load +(async function() { + + getStatus(); + +})(); + +// Get current status +async function getStatus() { + + try { + + const response = await fetch(config.licenseDataUrl + "/status"); + const data = await response.json(); + + renderStatus(data); + + // Refresh on interval + setTimeout(getStatus, config.statusRefreshInterval * 1000); + + } catch (error){ + console.log(error); + } + +} + +// Display status +const versionNumber = document.getElementById("version-number"); +const sourceUrl = document.getElementById("source-url"); + +function renderStatus(data){ + + // Show clean source URL + let source = config.licenseDataUrl.replace(/^(https?:|)\/\//, ""); + source = source.substring(0, source.indexOf(":")); + sourceUrl.innerHTML = source; + + // Version number + const majorVersion = data.version.major; + const minorVersion = data.version.minor; + versionNumber.innerHTML = `${majorVersion}.${minorVersion}`; + +} diff --git a/js/web-api.js b/js/web-api.js new file mode 100644 index 0000000..56bfe22 --- /dev/null +++ b/js/web-api.js @@ -0,0 +1,184 @@ +const SERVER_URL = config.serverUrl; +const GROUP_ALIAS = config.groupAlias; +const CREDENTIALS = config.credentials; + +const DOCUMENTS_TOTAL_SESSION_KEY = "documentsTotal"; +const DOCUMENTS_QUEUED_SESSION_KEY = "documentsQueued"; + +// DriveWorks Live Web API +const DW_CLIENT = new window.DriveWorksLiveClient(SERVER_URL); +let login; + +// Render on load +(async function () { + + if (SERVER_URL && GROUP_ALIAS){ + + try { + + // Login to Group + login = await DW_CLIENT.loginGroup(GROUP_ALIAS, CREDENTIALS); + + // Get data from API + getApiData(); + + } catch (error) { + console.log(error); + } + + } + +})(); + +// Get data +async function getApiData(){ + + try { + + // Get Project and Specification data from API + await Promise.all([ + getProjects(), + getSpecifications(), + getSpecificationsQueued() + ]); + + // Refresh data on interval + setTimeout(getApiData, config.apiRefreshInterval * 1000); + + } catch (error) { + console.log(error); + } + +} + +// Get project data +async function getProjects(){ + + const total = document.getElementById("projects-total"); + + try { + + const projects = await DW_CLIENT.getProjects(GROUP_ALIAS); + total.innerHTML = projects.length; + + } catch (error) { + console.log(error); + } + +} + +// Get specification data +async function getSpecifications(){ + + const total = document.getElementById("specifications-total"); + + try { + + const specifications = await DW_CLIENT.getAllSpecifications(GROUP_ALIAS); + total.innerHTML = specifications.length; + + getDocuments(specifications); + + } catch (error) { + console.log(error); + } + +} + +async function getSpecificationsQueued(){ + + const output = document.getElementById("specifications-queued"); + + // Show totals (if previously saved) + if (sessionStorage.getItem("specificationsQueued")){ + output.innerHTML = `In Queue: ${storedGeneratingTotal}`; + } + + try { + + // Get queued specifications (in "Automatic" state) + const queuedSpecifications = await DW_CLIENT.getAllSpecifications(GROUP_ALIAS, "$filter=StateType eq 'Automatic'"); + output.innerHTML = `In Queue: ${queuedSpecifications.length}`; + + // Story + sessionStorage.getItem("specificationsQueued") + + } catch (error) { + console.log(error); + } + +} + +// Get documents data +const totalOutput = document.getElementById("documents-total"); +const queueOutput = document.getElementById("documents-queued"); +let documentsTotal, storedDocumentTotal; +let queuedTotal, storedQueuedTotal; + +// Get documents data +async function getDocuments(specifications){ + + const documentRequests = []; + storedDocumentTotal = sessionStorage.getItem(DOCUMENTS_TOTAL_SESSION_KEY); + storedQueuedTotal = sessionStorage.getItem(DOCUMENTS_QUEUED_SESSION_KEY); + documentsTotal = 0; + queuedTotal = 0; + + // Show stored totals (if previously saved) + if (storedDocumentTotal){ + totalOutput.innerHTML = storedDocumentTotal; + } + if (storedQueuedTotal){ + queueOutput.innerHTML = `In Queue: ${storedQueuedTotal}`; + } + + // Generate requests for document total of each spec (and accumulate) + for (const specification of specifications) { + documentRequests.push(getDocumentsFromSpecification(specification.id)); + } + + // Process specifications + try { + + // Request all specification document totals + await Promise.all(documentRequests); + + // Output final totals + totalOutput.innerHTML = documentsTotal; + queueOutput.innerHTML = `In Queue: ${queuedTotal}`; + + // Store totals in session + sessionStorage.setItem(DOCUMENTS_TOTAL_SESSION_KEY, documentsTotal); + sessionStorage.setItem(DOCUMENTS_QUEUED_SESSION_KEY, queuedTotal); + + } catch (error) { + console.log(error); + } + +} + +// Get documents data +async function getDocumentsFromSpecification(specification){ + + // Get document data + const documents = await DW_CLIENT.getSpecificationDocuments(GROUP_ALIAS, specification); + + // Increment total + documentsTotal += documents.length; + + // Check for generating files + for (let i = 0; i < documents.length; i++) { + if (documents[i].fileExists === false){ + queuedTotal++; + } + } + + // Increase visual output (count up, if no current total shown) + if (!storedDocumentTotal){ + totalOutput.innerHTML = documentsTotal; + } + if (!storedQueuedTotal){ + queueOutput.innerHTML = `In Queue: ${queuedTotal}`; + } + +}