Compare commits

..

94 Commits

Author SHA1 Message Date
1fa85dacb4 change port to 80
this way the server is reachable from the ot network
2024-11-25 16:10:59 +01:00
64725de8b9 use the primary color (blue) for save buttons instead of the Amazon orange 2024-10-13 19:42:39 +02:00
768962aa43 remove daterangepicker and moment JS
I replaced moment with luxon.
And I am working on my own date picker.
2024-10-13 19:05:46 +02:00
1fd7becf9a remove unused imports after adding lazy loading 2024-10-13 18:43:44 +02:00
e292587a36 update router to use standalone components and lazy loading 2024-10-13 10:54:33 +02:00
8fef666183 step 3 - Bootstrap the application using standalone APIs
see https://v17.angular.io/guide/standalone-migration
I executed
ng g @angular/core:standalone
and select "Bootstrap the project using standalone APIs"

Note Step 2 did not change any files.
2024-10-13 10:22:18 +02:00
08a481b5ba step 1 - convert al lcomponents, directives and pipes to standalone
see https://v17.angular.io/guide/standalone-migration
I executed:
ng g @angular/core:standalone
and selected "Convert all components, directives and pipes to standalone"
2024-10-13 09:55:07 +02:00
526e1d842e update Gradle to 8.10.2 2024-09-29 08:59:48 +02:00
06c9b4998f update Junit to 5.11.1 2024-09-29 08:57:48 +02:00
ac5dcdc58f Merge pull request 'dashboard' (#1) from dashboard into master
Reviewed-on: #1
2024-09-29 06:47:35 +00:00
296d42e721 update node to 20.17.0 2024-09-29 08:45:56 +02:00
9db020ceb0 update 3rd party libs 2024-09-29 08:21:46 +02:00
36da503be9 add date picker to dashboard page 2024-09-29 08:11:38 +02:00
42751f84d4 remove head-box dif, because it is unnecessary 2024-09-28 10:35:15 +02:00
39d7c029ea remove debug output for absolute date 2024-09-28 10:28:28 +02:00
f072185074 nicer placement of the date picker 2024-09-28 10:26:53 +02:00
122ba11a79 remove debugger 2024-09-28 10:26:38 +02:00
f1d7799bf1 update luxon to 3.5.0 2024-09-28 09:18:23 +02:00
680f1bff03 update Angular Material to 18.2 2024-09-28 09:16:56 +02:00
fa0315650a update Angular to 18.2 2024-09-28 09:14:31 +02:00
8f765dd478 remove date picker test component 2024-09-28 09:12:31 +02:00
1234560512 update ngx-markdown to be compatible with angular 2024-09-28 08:54:47 +02:00
2711579afb add resource handler to be able to load htm/js
With the Angular update the smart people at google moved the
location where the build output is written to. It is now in a
subfolder called 'browser'. This means the default lookup locations
of Spring don't work anymore.
2024-09-28 08:37:47 +02:00
3ac021e45f ng update @angular/material@18 2024-07-28 12:17:53 +02:00
c199eae4ff g update @angular/core@18 @angular/cli@18 2024-07-28 12:17:00 +02:00
6073dd0779 fix compile errors after angualr 17 update 2024-07-28 12:14:19 +02:00
fee5eda780 update ngx-markdown 2024-07-28 11:58:01 +02:00
cc0db6d732 update angular material to 17 2024-07-28 11:56:46 +02:00
f084396e95 update angular cor and cli to 17 2024-07-28 11:55:22 +02:00
e4b6eea4b1 angular updates are shit 2024-07-28 11:54:09 +02:00
75fa966af3 udpate Spring to 3.3.2 2024-07-28 11:47:05 +02:00
a3e4917e89 update gradle versions plugin to 0.51.0 2024-07-27 14:09:37 +02:00
05fc03e48f update to gradle 8.9 2024-07-27 14:05:36 +02:00
b00ce507ef update to jdk 21 2024-07-27 14:02:37 +02:00
a69fe09464 more updates to handle date ranges with my custom range language 2024-07-27 13:19:35 +02:00
77b99801e4 add projects directly instead of scanning for them
IntelliJ Idea cannot import projects unless they are listed in the
settings.gradle
2024-05-05 10:27:49 +02:00
da210145e6 opt out of analytics 2024-05-05 10:23:33 +02:00
21a84b5223 use date picker in visualization page 2024-05-05 10:22:45 +02:00
a99a884423 add date parser for relative time notation 2024-05-05 08:40:30 +02:00
6d6b6ba00c add date picker component 2024-04-25 09:41:10 +02:00
380bad6967 update testing properties 2024-04-03 16:46:25 +02:00
f3556b6909 custom ticks for no unit y-axis 2024-04-03 16:46:25 +02:00
452030de5e use more efficient way to determine needed bytes for variable byte
encoding

I experimented with a few branch-free variants, but they were slower
than just using ternary operators and readable code.
2024-04-01 12:44:30 +02:00
6b8e3d2089 add component for a custom date picker that also knows relative date ranges like 'last month' 2024-03-31 14:48:33 +02:00
b0467c4571 better scrolling of legend 2023-10-03 14:12:59 +02:00
a64e851c33 define max size for legend 2023-10-01 14:33:52 +02:00
fefc7411c1 add source-map-explorer 2023-10-01 10:31:51 +02:00
00a1bf8ffb update to Java 21 2023-10-01 09:30:36 +02:00
317d31bbda update gradle to 8.3 2023-10-01 09:21:07 +02:00
d339f5307f update node to 18.18.0 2023-10-01 09:09:40 +02:00
b3085c9b0c Merge commit '731e9264e334043d2f36bc35bfd0231447dfb8dc' into dashboard 2023-09-30 20:27:31 +02:00
96955e0515 remove moment from angular build 2023-09-30 20:26:06 +02:00
9a41f132f8 angular update to 16 step 4 2023-09-30 18:16:37 +02:00
33c4fe1448 angular update to 16 step3 2023-09-30 17:54:19 +02:00
d320ff3d93 angular update to 16 step 2 2023-09-30 17:51:40 +02:00
4ce9cca7e0 angular update to 16 step 1 2023-09-30 17:48:26 +02:00
29215f0410 update gradle node plugin 2023-09-30 17:30:08 +02:00
abc60c6de2 update antlr to 4.13.1 2023-09-30 17:26:44 +02:00
c969c5c848 update libs 2023-09-30 17:23:33 +02:00
6552d51bcc there is no liquishit in this project 2023-09-30 17:14:58 +02:00
43e13b53b1 make the legend movable
The legend ("key" in Gnuplot speak) is no longer part of the image.
Instead it is a floating&movable overlay.

In the gallery we still use the legend/key in the image.
2023-09-30 17:12:49 +02:00
731e9264e3 move database to c drive 2023-05-25 08:56:19 +02:00
8839ab52a2 add nexus to be able to build in disco.lab 2023-05-25 08:55:26 +02:00
f8a199fd6a move edit/delete buttons outside the text 2023-04-04 19:08:52 +02:00
48ae47d050 add second column to dashboard 2023-04-04 19:02:46 +02:00
3386f0994f add preview for markup 2023-04-04 18:46:34 +02:00
75f45c4d87 only show external link icon for external links 2023-03-23 18:14:24 +01:00
bacd86d836 add markdown support for text widgets 2023-03-19 20:10:04 +01:00
c7af333052 remove title form add/edit plot dialog 2023-03-19 09:18:16 +01:00
a3aa62aee2 abort plotting on dashboards 2023-03-19 09:16:50 +01:00
6d2e8da805 add more icons we might need some time 2023-03-18 09:54:03 +01:00
53e7a21602 show edit of title and descr only when hovering 2023-03-18 09:46:03 +01:00
b6045eda22 open visualization page with settings 2023-03-18 09:42:04 +01:00
6d8af4fdc6 hide gallery when it is not active 2023-03-17 17:08:55 +01:00
359c17bf29 add unit type for bytes 2023-03-17 16:53:34 +01:00
686e3edd60 last 3 month does not include current month 2023-03-16 19:08:47 +01:00
2310c2ab0d position invaders after screen resize 2023-03-16 18:55:19 +01:00
a6fbd0c60d show invaders again during plotting 2023-03-16 18:20:35 +01:00
2b82a6822c fix redirect for dashboard page 2023-03-16 17:53:23 +01:00
0bc58ba166 add more ticks 2023-03-16 17:49:40 +01:00
e543e0b388 form validation for new dashboard dialog 2023-03-14 20:09:36 +01:00
882f04d893 set focus the new way 2023-03-13 19:41:42 +01:00
da4a95e5ed fill intervalUnit when editing plot 2023-03-13 19:39:00 +01:00
4679da480c replace randomUUID with something that is available in real live 2023-03-12 20:15:29 +01:00
1ca4f18e3d a somewhat working example of a dashboard 2023-03-12 17:33:26 +01:00
add30a5ee9 use a drag handle with icon 2023-03-12 17:02:40 +01:00
e42c00cc08 update libraries 2023-03-12 11:43:17 +01:00
c9758adbef increase socket timeout
The old value of 2ms lead to 45% load for a single core
2023-03-12 11:04:27 +01:00
dafe6813ed enable save button only if dashboard is dirty 2023-03-12 09:36:33 +01:00
bc0ae23de5 delete plot from dashboard 2023-03-12 09:15:56 +01:00
02c748c286 delete text from dashboard 2023-03-12 09:11:49 +01:00
44aed2883d edit name and description on dashboard page 2023-03-12 08:52:54 +01:00
96ed788793 cleanup 2023-03-12 08:32:42 +01:00
b5028e03be edit plots 2023-03-12 08:24:16 +01:00
116 changed files with 19277 additions and 12309 deletions

View File

@@ -1,6 +1,6 @@
package org.lucares.pdb.map; package org.lucares.pdb.map;
import java.util.List; import org.lucares.utils.HumanBytes;
public class PersistentMapStats { public class PersistentMapStats {
private long values = 0; private long values = 0;
@@ -87,7 +87,7 @@ public class PersistentMapStats {
builder.append(String.format("\navg. depth= %.2f", averageDepth)); builder.append(String.format("\navg. depth= %.2f", averageDepth));
builder.append(String.format("\navg. fill= %.2f", averageFill)); builder.append(String.format("\navg. fill= %.2f", averageFill));
builder.append(String.format("\nvalues/node=%.2f", averageValuesInNode)); builder.append(String.format("\nvalues/node=%.2f", averageValuesInNode));
builder.append(String.format("\nfile size= %s\n", toHumanBytes(fileSize))); builder.append(String.format("\nfile size= %s\n", HumanBytes.toHumanBytes(fileSize)));
return builder.toString(); return builder.toString();
} }
@@ -100,21 +100,9 @@ public class PersistentMapStats {
builder.append(String.format("\navg. depth= %.2f -> %.2f", old.averageDepth, averageDepth)); builder.append(String.format("\navg. depth= %.2f -> %.2f", old.averageDepth, averageDepth));
builder.append(String.format("\navg. fill= %.2f -> %.2f", old.averageFill, averageFill)); builder.append(String.format("\navg. fill= %.2f -> %.2f", old.averageFill, averageFill));
builder.append(String.format("\nvalues/node=%.2f -> %.2f", old.averageValuesInNode, averageValuesInNode)); builder.append(String.format("\nvalues/node=%.2f -> %.2f", old.averageValuesInNode, averageValuesInNode));
builder.append(String.format("\nfile size= %s -> %s\n", toHumanBytes(old.fileSize), toHumanBytes(fileSize))); builder.append(String.format("\nfile size= %s -> %s\n", HumanBytes.toHumanBytes(old.fileSize),
HumanBytes.toHumanBytes(fileSize)));
return builder.toString(); return builder.toString();
} }
private static String toHumanBytes(final long bytes) {
final List<String> powers = List.of("bytes", "KB", "MB", "GB", "TB", "PB", "EB");
int power = 1;
String result = String.format("%d bytes", bytes);
while (bytes >= Math.pow(1024, power) && power < powers.size()) {
result = String.format("%.3f", bytes / Math.pow(1024, power));
result = result.replaceAll("\\.?0*$", "");
result = result + " " + powers.get(power);
power = power + 1;
}
return result;
}
} }

View File

@@ -4,27 +4,27 @@ import org.apache.tools.ant.filters.ReplaceTokens
plugins { plugins {
id 'java' id 'java'
id 'eclipse' id 'eclipse'
id 'com.github.ben-manes.versions' version "0.45.0" // check for dependency updates run: gradlew dependenyUpdates id 'com.github.ben-manes.versions' version "0.51.0" // check for dependency updates run: gradlew dependenyUpdates
} }
ext { ext {
javaVersion=17 javaVersion=21
version_log4j2= '2.19.0' // keep in sync with spring-boot-starter-log4j2 version_log4j2= '2.20.0' // keep in sync with spring-boot-starter-log4j2
version_spring = '3.0.2' version_spring = '3.3.4'
version_junit = '5.9.2' version_junit = '5.11.1'
version_junit_platform = '1.9.2' version_junit_platform = '1.11.1'
version_nodejs = '16.17.1' // keep in sync with npm version_nodejs = '20.17.0' // keep in sync with npm
version_npm = '8.15.0' // keep in sync with nodejs version_npm = '10.8.2' // keep in sync with nodejs
lib_antlr = "org.antlr:antlr4:4.11.1" lib_antlr = "org.antlr:antlr4:4.13.2"
lib_commons_collections4 = 'org.apache.commons:commons-collections4:4.4' lib_commons_collections4 = 'org.apache.commons:commons-collections4:4.4'
lib_commons_csv= 'org.apache.commons:commons-csv:1.10.0' lib_commons_csv= 'org.apache.commons:commons-csv:1.12.0'
lib_commons_lang3 = 'org.apache.commons:commons-lang3:3.12.0' lib_commons_lang3 = 'org.apache.commons:commons-lang3:3.17.0'
lib_jackson_databind = 'com.fasterxml.jackson.core:jackson-databind:2.14.2' lib_jackson_databind = 'com.fasterxml.jackson.core:jackson-databind:2.18.0'
lib_log4j2_core = "org.apache.logging.log4j:log4j-core:${version_log4j2}" lib_log4j2_core = "org.apache.logging.log4j:log4j-core:${version_log4j2}"
lib_log4j2_slf4j_impl = "org.apache.logging.log4j:log4j-slf4j-impl:${version_log4j2}" lib_log4j2_slf4j_impl = "org.apache.logging.log4j:log4j-slf4j-impl:${version_log4j2}"
@@ -73,6 +73,11 @@ subprojects {
url 'https://repo.lucares.de/' url 'https://repo.lucares.de/'
content { includeGroup "org.lucares" } content { includeGroup "org.lucares" }
} }
maven {
url "https://nexus.disco.lab/repository/maven-all/"
allowInsecureProtocol = true
content { excludeGroup "org.lucares" }
}
mavenCentral(content: { excludeGroup "org.lucares" }) mavenCentral(content: { excludeGroup "org.lucares" })
} }
@@ -136,5 +141,5 @@ subprojects {
} }
wrapper { wrapper {
gradleVersion = '8.0.1' gradleVersion = '8.10.2'
} }

Binary file not shown.

View File

@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

34
gradlew vendored
View File

@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,10 +85,9 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. ' "$PWD" ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -133,10 +134,13 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shell script including quotes and variable substitutions, so put them in DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded. # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored
View File

@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View File

@@ -1,5 +1,7 @@
package org.lucares.pdb.api; package org.lucares.pdb.api;
import java.util.concurrent.TimeUnit;
public class AbortException extends RuntimeException { public class AbortException extends RuntimeException {
private static final long serialVersionUID = 7614132985675048490L; private static final long serialVersionUID = 7614132985675048490L;
@@ -29,4 +31,18 @@ public class AbortException extends RuntimeException {
} }
} }
public static void sleepAbortibly(final long millis) throws AbortException {
final long deadline = System.currentTimeMillis() + millis;
while (System.currentTimeMillis() < deadline) {
try {
TimeUnit.MILLISECONDS.sleep(Math.min(10, deadline - System.currentTimeMillis()));
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new AbortException();
}
AbortException.abortIfInterrupted();
}
}
} }

2
pdb-js/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -18,12 +18,15 @@
"prefix": "app", "prefix": "app",
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:application",
"options": { "options": {
"outputPath": "build/generated/resources", "outputPath": {
"base": "build/generated/resources"
},
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "polyfills": [
"polyfills": "src/polyfills.ts", "src/polyfills.ts"
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": [
@@ -33,7 +36,10 @@
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": [
"node_modules/marked/marked.min.js"
],
"browser": "src/main.ts"
}, },
"configurations": { "configurations": {
"production": { "production": {
@@ -55,12 +61,11 @@
"with": "src/environments/environment.prod.ts" "with": "src/environments/environment.prod.ts"
} }
], ],
"outputHashing": "all" "outputHashing": "all",
"sourceMap": true
}, },
"development": { "development": {
"buildOptimizer": false,
"optimization": false, "optimization": false,
"vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true, "sourceMap": true,
"namedChunks": true "namedChunks": true
@@ -75,10 +80,10 @@
}, },
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "pdb-js:build:production" "buildTarget": "pdb-js:build:production"
}, },
"development": { "development": {
"browserTarget": "pdb-js:build:development" "buildTarget": "pdb-js:build:development"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
@@ -86,7 +91,7 @@
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "pdb-js:build" "buildTarget": "pdb-js:build"
} }
}, },
"test": { "test": {
@@ -109,5 +114,8 @@
} }
} }
} }
},
"cli": {
"analytics": false
} }
} }

View File

@@ -2,7 +2,8 @@ import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
plugins { plugins {
id("com.github.node-gradle.node") version "3.5.1" id("com.github.node-gradle.node") version "7.0.0"
id("java-library") // not sure why this is needed - is already set in /build.gradle - but without it the project sometimes (not always) is not configured as a java project
} }

25324
pdb-js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,38 +9,44 @@
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e", "e2e": "ng e2e",
"releasebuild": "ng build --configuration production" "releasebuild": "ng build --configuration production",
"explore": "source-map-explorer build/generated/resources/**/*.js"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^15.0.2", "@angular/animations": "^18.2.6",
"@angular/cdk": "^15.0.1", "@angular/cdk": "^18.2.6",
"@angular/common": "^15.0.2", "@angular/common": "^18.2.6",
"@angular/compiler": "^15.0.2", "@angular/compiler": "^18.2.6",
"@angular/core": "^15.0.2", "@angular/core": "^18.2.6",
"@angular/forms": "^15.0.2", "@angular/forms": "^18.2.6",
"@angular/material": "^15.0.1", "@angular/material": "^18.2.6",
"@angular/platform-browser": "^15.0.2", "@angular/platform-browser": "^18.2.6",
"@angular/platform-browser-dynamic": "^15.0.2", "@angular/platform-browser-dynamic": "^18.2.6",
"@angular/router": "^15.0.2", "@angular/router": "^18.2.6",
"moment": "^2.29.1", "luxon": "^3.4.3",
"marked": "^12",
"ngx-markdown": "18.0.0",
"rxjs": "~7.5.0", "rxjs": "~7.5.0",
"rxjs-compat": "^6.6.7", "rxjs-compat": "^6.6.7",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.11.4" "zone.js": "^0.14.10"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^15.0.2", "@angular-devkit/build-angular": "^18.2.6",
"@angular/cli": "^15.0.2", "@angular/cli": "^18.2.6",
"@angular/compiler-cli": "^15.0.2", "@angular/compiler-cli": "^18.2.6",
"@types/jasmine": "~3.10.0", "@types/jasmine": "~4.3.0",
"@types/luxon": "^3.3.2",
"@types/marked": "^4.0.8",
"@types/node": "^12.11.1", "@types/node": "^12.11.1",
"jasmine-core": "~3.10.0", "jasmine-core": "~4.6.0",
"karma": "~6.3.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.1.0", "karma-coverage": "~2.2.0",
"karma-jasmine": "~4.0.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~1.7.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "4.8" "source-map-explorer": "^2.5.3",
"typescript": "^5.4.5"
} }
} }

View File

@@ -1,31 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { VisualizationPageComponent } from './visualization-page/visualization-page.component';
import { MainPageComponent } from './main-page/main-page.component';
import { UploadPageComponent } from './upload-page/upload-page.component';
import { HelpPageComponent } from './help-page/help-page.component';
import { DashboardPageComponent } from './dashboard-page/dashboard-page.component';
import { DashboardComponent } from './dashboard-page/dashboard/dashboard.component';
import { CustomizableGridComponent } from './customizable-grid/customizable-grid.component';
const routes: Routes = [
{ path: '', component: MainPageComponent},
{ path: 'vis', component: VisualizationPageComponent },
{ path: 'dashboard', component: DashboardPageComponent},
{ path: 'dashboard/:id', component: DashboardComponent},
{ path: 'upload', component: UploadPageComponent },
{ path: 'grid', component: CustomizableGridComponent },
{ path: 'help', component: HelpPageComponent },
// { path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {})
],
//declarations: [VisualizationPageComponent],
declarations: [],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -5,13 +5,11 @@ import { AppComponent } from './app.component';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
RouterTestingModule RouterTestingModule,
],
declarations: [
AppComponent AppComponent
], ],
}).compileComponents(); }).compileComponents();
})); }));
it('should create the app', () => { it('should create the app', () => {

View File

@@ -1,11 +1,15 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser'; import {DomSanitizer} from '@angular/platform-browser';
import {MatIconRegistry} from '@angular/material/icon'; import {MatIconRegistry} from '@angular/material/icon';
import { MatAnchor } from '@angular/material/button';
import { RouterLink, RouterOutlet } from '@angular/router';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss'],
standalone: true,
imports: [MatAnchor, RouterLink, RouterOutlet]
}) })
export class AppComponent { export class AppComponent {
title = 'pdb'; title = 'pdb';

View File

@@ -1,105 +0,0 @@
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule, enableProdMode } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MainPageComponent } from './main-page/main-page.component';
import { HelpPageComponent } from './help-page/help-page.component';
import { UploadPageComponent } from './upload-page/upload-page.component';
import { VisualizationPageComponent } from './visualization-page/visualization-page.component';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatButtonModule} from '@angular/material/button';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatSelectModule} from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatRadioModule} from '@angular/material/radio';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatTooltipModule} from '@angular/material/tooltip';
import { YAxisDefinitionComponent } from './y-axis-definition/y-axis-definition.component';
import { QueryAutocompleteComponent } from './query-autocomplete/query-autocomplete.component';
import { LimitByComponent } from './limit-by/limit-by.component';
import { PlotDetailsComponent } from './plot-details/plot-details.component';
import { PlotViewComponent } from './plot-view/plot-view.component';
import { GalleryViewComponent, GalleryItemView, GalleryFilterView } from './gallery-view/gallery-view.component';
import { ImageToggleComponent } from './image-toggle/image-toggle.component';
import { DashboardPageComponent } from './dashboard-page/dashboard-page.component';
import { NewDashboardComponent } from './dashboard-page/new-dashboard/new-dashboard.component';
import { MatDialogModule, MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog';
import {MatTableModule} from '@angular/material/table';
import {MatBadgeModule} from '@angular/material/badge';
import { DashboardComponent } from './dashboard-page/dashboard/dashboard.component';
import { AddTextDialogComponent } from './dashboard-page/dashboard/add-text-dialog/add-text-dialog.component';
import { TextWidgetComponent } from './dashboard-page/dashboard/text-widget/text-widget.component';
import { AddPlotDialogComponent } from './dashboard-page/dashboard/add-plot-dialog/add-plot-dialog.component';
import { PlotWidgetComponent } from './dashboard-page/dashboard/plot-widget/plot-widget.component';
import { FullScreenPlotDialogComponent } from './dashboard-page/dashboard/full-screen-plot-dialog/full-screen-plot-dialog.component';
import { CustomizableGridComponent } from './customizable-grid/customizable-grid.component';
import {DragDropModule} from '@angular/cdk/drag-drop';
import { ConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
import { FocusDirective } from './focus.directive';
@NgModule({
declarations: [
AppComponent,
MainPageComponent,
HelpPageComponent,
UploadPageComponent,
VisualizationPageComponent,
YAxisDefinitionComponent,
QueryAutocompleteComponent,
LimitByComponent,
PlotDetailsComponent,
PlotViewComponent,
GalleryViewComponent,
GalleryItemView,
GalleryFilterView,
ImageToggleComponent,
DashboardPageComponent,
NewDashboardComponent,
DashboardComponent,
AddTextDialogComponent,
TextWidgetComponent,
AddPlotDialogComponent,
PlotWidgetComponent,
FullScreenPlotDialogComponent,
CustomizableGridComponent,
ConfirmationDialogComponent,
FocusDirective
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
DragDropModule,
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatRadioModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatSelectModule,
MatSnackBarModule,
MatTableModule,
MatTooltipModule,
BrowserAnimationsModule,
HttpClientModule
],
providers: [{provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {hasBackdrop: true}}],
bootstrap: [AppComponent]
})
export class AppModule { }
enableProdMode()

View File

@@ -0,0 +1,188 @@
<style>
.date-picker-overlay {
width: 500px;
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
background-color: white;
}
.tab-quick {
display: flex;
flex-direction: row;
gap: 2em;
}
.tab-quick-column {
display: flex;
flex-direction: column;
}
.date-picker-form-field {
width: 23.5em;
}
</style>
<button
mat-button
matTooltip="Date Picker"
(click)="isOpen = !isOpen"
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
[attr.disabled]="isDisabled ? 'disabled' : null"
>
{{ datePickerControl.value?.display }}
</button>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isOpen"
>
<div class="date-picker-overlay">
<mat-tab-group
animationDuration="0ms"
(selectedTabChange)="tabChange()"
[(selectedIndex)]="selectedTabIndex"
>
<mat-tab label="Quick">
<div class="tab-quick">
<div class="tab-quick-column">
<button mat-button (click)="applyQuick('BD/E1D', 'today')">
Today
</button>
<button mat-button (click)="applyQuick('B-1D/E-1D', 'yesterday')">
Yesterday
</button>
<button mat-button (click)="applyQuick('BW/EW', 'this week')">
This Week
</button>
<button mat-button (click)="applyQuick('BM/EM', 'this month')">
This Month
</button>
<button mat-button (click)="applyQuick('BY/EY', 'this year')">
This Year
</button>
</div>
<div class="tab-quick-column">
<button mat-button (click)="applyQuick('B-7D/ED', 'last 7 days')">
Last 7 Days
</button>
<button mat-button (click)="applyQuick('B-1W/ED', 'last week')">
Last Week
</button>
<button mat-button (click)="applyQuick('B-30D/ED', 'last 30 days')">
Last 30 Days
</button>
<button mat-button (click)="applyQuick('B-1M/E-1M', 'last month')">
Last Month
</button>
<button
mat-button
(click)="applyQuick('B-3M/E-1M', 'last 3 months')"
>
Last 3 Months
</button>
<button mat-button (click)="applyQuick('B-1Y/E-1Y', 'last year')">
Last Year
</button>
</div>
</div>
</mat-tab>
<mat-tab label="Relative">
<mat-form-field
class="pdb-form-number-small"
(wheel)="scrollRelativeTimeRange($event, 'seconds', 59)"
>
<mat-label>Seconds:</mat-label>
<input
matInput
name="relative-time-range-seconds"
[(ngModel)]="relativeTimeRange.seconds"
type="number"
min="0"
max="59"
/>
</mat-form-field>
<mat-form-field
class="pdb-form-number-small"
(wheel)="scrollRelativeTimeRange($event, 'minutes', 59)"
>
<mat-label>Minutes:</mat-label>
<input
matInput
name="relative-time-range-minutes"
[(ngModel)]="relativeTimeRange.minutes"
type="number"
min="0"
max="59"
/>
</mat-form-field>
<mat-form-field
class="pdb-form-number-small"
(wheel)="scrollRelativeTimeRange($event, 'hours', 23)"
>
<mat-label>Hours:</mat-label>
<input
matInput
name="relative-time-range-hours"
[(ngModel)]="relativeTimeRange.hours"
type="number"
min="0"
max="23"
/>
</mat-form-field>
<mat-form-field
class="pdb-form-number-small"
(wheel)="scrollRelativeTimeRange($event, 'days', 367)"
>
<mat-label>Days:</mat-label>
<input
matInput
name="relative-time-range-days"
[(ngModel)]="relativeTimeRange.days"
type="number"
min="0"
max="367"
/>
</mat-form-field>
<mat-form-field
class="pdb-form-number-small"
(wheel)="scrollRelativeTimeRange($event, 'months', 11)"
>
<mat-label>Months:</mat-label>
<input
matInput
name="relative-time-range-months"
[(ngModel)]="relativeTimeRange.months"
type="number"
min="0"
max="11"
/>
</mat-form-field>
<mat-form-field
class="pdb-form-number-small"
(wheel)="scrollRelativeTimeRange($event, 'years', 10)"
>
<mat-label>Years:</mat-label>
<input
matInput
name="relative-time-range-years"
[(ngModel)]="relativeTimeRange.years"
type="number"
min="0"
max="10"
/>
</mat-form-field>
<button mat-button (click)="applyRelativeTimeRange()">Apply</button>
</mat-tab>
<mat-tab label="Absolute">
<mat-form-field class="date-picker-form-field">
<mat-label>Date Range:</mat-label>
<input matInput [formControl]="dateRange" name="dates" />
</mat-form-field>
<button mat-button (click)="applyAbsoluteTime()">Apply</button>
</mat-tab>
</mat-tab-group>
</div>
</ng-template>

View File

@@ -0,0 +1,198 @@
import {
Component,
EventEmitter,
forwardRef,
Input,
Output,
} from "@angular/core";
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, Validators, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { MatButton } from "@angular/material/button";
import { MatTooltip } from "@angular/material/tooltip";
import { CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay";
import { MatTabGroup, MatTab } from "@angular/material/tabs";
import { MatFormField, MatLabel } from "@angular/material/form-field";
import { MatInput } from "@angular/material/input";
export type DateType = "QUICK" | "RELATIVE" | "ABSOLUTE";
export class DateValue {
constructor(
public type: DateType,
public value: string,
public display: string,
) {}
}
export class DatePickerChange {
constructor(public value: DateValue) {}
}
@Component({
selector: "app-date-picker",
templateUrl: "./date-picker.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatePickerComponent),
multi: true,
},
],
standalone: true,
imports: [
MatButton,
MatTooltip,
CdkOverlayOrigin,
CdkConnectedOverlay,
MatTabGroup,
MatTab,
MatFormField,
MatLabel,
MatInput,
FormsModule,
ReactiveFormsModule,
],
})
export class DatePickerComponent implements ControlValueAccessor {
isOpen = false;
relativeTimeRangeUnit = "relativeTimeRangeMinutes";
relativeTimeRangeAmount = 15;
relativeTimeRange = {
seconds: 0,
minutes: 15,
hours: 0,
days: 0,
months: 0,
years: 0,
};
dateRange = new FormControl<string>(
"2019-10-05 00:00:00 - 2019-10-11 23:59:59",
[
Validators.pattern(
/^\d{4}-\d{2}-\d{2} ([01][0-9]|2[0-3]):\d{2}:\d{2} - \d{4}-\d{2}-\d{2} ([01][0-9]|2[0-3]):\d{2}:\d{2}$/,
),
],
);
datePickerControl = new FormControl(
new DateValue("QUICK", "BM/EM", "this month"),
);
@Input()
isDisabled: boolean = false;
@Output()
readonly dateValueSelected: EventEmitter<DatePickerChange> = new EventEmitter<
DatePickerChange
>();
selectedTabIndex = 0;
_onChange = (_: any) => {};
_onTouched = (_: any) => {};
constructor() {}
getDateValue(): DateValue {
return this.datePickerControl.value!;
}
writeValue(obj: DateValue): void {
this.datePickerControl.setValue(obj);
switch (obj.type) {
case "QUICK":
break;
case "ABSOLUTE":
this.dateRange.setValue(obj.value);
break;
case "RELATIVE":
const x = this.relativeTimeRange;
// obj.value looks like "P1Y2M3DT4H5M6S" or "PT4H5M6S" or "P1Y2M3D" or "P1YT6S" or ...
const matches = obj.value.match(
/P(?:(\d+)Y)(?:(\d+)M)(?:(\d+)D)?(?:T(?:(\d+)H)(?:(\d+)M)(?:(\d+)S))?/,
) ?? [];
x.years = Number.parseInt(matches[1] ?? 0);
x.months = Number.parseInt(matches[2] ?? 0);
x.days = Number.parseInt(matches[3] ?? 0);
x.hours = Number.parseInt(matches[4] ?? 0);
x.minutes = Number.parseInt(matches[5] ?? 0);
x.seconds = Number.parseInt(matches[6] ?? 0);
break;
default:
}
}
registerOnChange(fn: any): void {
this._onChange = fn;
}
registerOnTouched(fn: any): void {
this._onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.isDisabled = isDisabled;
}
dateDisplay(): string {
return this.datePickerControl.value?.display || "no date set";
}
tabChange() {
//(<any> window).initSimpleDatePicker(); // breaks form control
}
setDateValue(dateValue: DateValue) {
this.datePickerControl.setValue(dateValue);
this._onChange(dateValue);
this.dateValueSelected.emit(new DatePickerChange(dateValue));
//console.log("date value updated: ", dateValue);
}
applyQuick(value: string, display: string) {
const newValue = new DateValue("QUICK", value, display);
this.setDateValue(newValue);
this.isOpen = false;
}
private fixToRange(val: number, min: number, max: number) {
return val < min ? min : (val > max ? max : val);
}
applyRelativeTimeRange() {
const x = this.relativeTimeRange;
const years = x.years ? "-"+x.years + "Y" : "";
const months = x.months ? "-"+x.months + "M" : "";
const days = x.days ? "-"+x.days + "D" : "";
const hours = x.hours ? "-"+x.hours + "H" : "";
const minutes = x.minutes ? "-"+x.minutes + "m" : "";
const timeRange = `B${years}${months}${days}${hours}${minutes}/Bm`;
const newValue = new DateValue("RELATIVE", timeRange, timeRange);
this.setDateValue(newValue);
this.isOpen = false;
}
applyAbsoluteTime() {
const value = <string> this.dateRange.value;
const newValue = new DateValue("ABSOLUTE", value, value);
this.setDateValue(newValue);
this.isOpen = false;
}
scrollRelativeTimeRange(
event: WheelEvent,
unit: "seconds" | "minutes" | "hours" | "days" | "months" | "years",
max: number,
) {
this.relativeTimeRange[unit] = this.fixToRange(
this.relativeTimeRange[unit] + (event.deltaY > 0 ? -1 : 1),
0,
max,
);
}
}

View File

@@ -8,8 +8,8 @@ describe('ConfirmationDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ ConfirmationDialogComponent ] imports: [ConfirmationDialogComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(ConfirmationDialogComponent); fixture = TestBed.createComponent(ConfirmationDialogComponent);

View File

@@ -1,9 +1,15 @@
import { Component, ElementRef, Inject, ViewChild } from '@angular/core'; import { Component, ElementRef, Inject, ViewChild } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose } from '@angular/material/dialog';
import { NgIf } from '@angular/common';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { MatButton } from '@angular/material/button';
import { FocusDirective } from '../focus.directive';
@Component({ @Component({
selector: 'app-confirmation-dialog', selector: 'app-confirmation-dialog',
templateUrl: './confirmation-dialog.component.html' templateUrl: './confirmation-dialog.component.html',
standalone: true,
imports: [NgIf, MatDialogTitle, CdkScrollable, MatDialogContent, MatDialogActions, MatButton, MatDialogClose, FocusDirective]
}) })
export class ConfirmationDialogComponent { export class ConfirmationDialogComponent {

View File

@@ -1,121 +1,113 @@
<style> <style>
.customizable-grid { .example-list {
width: 100%; list-style-type: none;
display: flex; padding: 0;
flex-wrap: wrap;
}
.draggable {
flex-shrink: 0;
padding: 0.5rem;
border: solid 1px;
border-collapse: collapse;
}
.draggable-small {
width: 300px;
height: 300px;
background-color: rgb(158, 240, 189);
}
.draggable-middle {
width: 600px;
height: 300px;
background-color: darksalmon;
}
.example-container {
width: 400px;
max-width: 100%;
margin: 0 25px 25px 0;
display: inline-block;
vertical-align: top;
} }
.example-list { .example-list li {
border: solid 1px #ccc; display: table-cell;
min-height: 60px; padding: 4px;
background: white; }
border-radius: 4px;
overflow: hidden; .example-container {
display: block; display: flex;
flex-wrap: wrap;
min-width: 600px;
max-width: 1200px;
} }
.example-box { .example-box {
padding: 20px 10px; width: var(--box-width);
border-bottom: solid 1px #ccc; height: var(--box-height);
border: solid 1px #ccc;
font-size: 30pt;
font-weight: bold;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
cursor: grab;
display: flex; display: flex;
flex-direction: row; justify-content: center;
align-items: center; align-items: center;
justify-content: space-between; text-align: center;
box-sizing: border-box; background: #fff;
cursor: move; border-radius: 4px;
background: white; position: relative;
font-size: 14px; z-index: 1;
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.example-box--wide {
width: 400px;
height: var(--box-height);
}
.example-box:active {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
opacity: 0.6;
}
.cdk-drop-list {
display: flex;
padding-right: 10px;
padding-bottom: 10px;
} }
.cdk-drag-preview { .cdk-drag-preview {
box-sizing: border-box; box-sizing: border-box;
border-color: red;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
0 3px 14px 2px rgba(0, 0, 0, 0.12);
} }
.cdk-drag-placeholder { .cdk-drag-placeholder {
opacity: 0; opacity: 0.3;
border-color: green;
} }
.cdk-drag-animating { .cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
} }
.example-box:last-child { .cdk-drop-list-dragging {
border: none; cursor: grabbing !important;
} }
.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) { button {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); margin-right: 4px;
} }
</style> </style>
<!-- <h1>Drag&Drop with a flex-wrap</h1>
<div class="customizable-grid">
<div class="draggable draggable-small"></div>
<div class="draggable draggable-middle"></div>
<div class="draggable draggable-small"></div>
<div class="draggable draggable-middle"></div>
</div>
-->
<div class="example-container"> <button (click)="add()">Add</button>
<h2>To do</h2> <button (click)="shuffle()">Shuffle</button>
<br />
<ul class="example-list">
<li *ngFor="let item of items">{{ item }}</li>
</ul>
<div
class="example-container"
cdkDropListGroup
[ngStyle]="{ '--box-width': boxWidth, '--box-height': boxHeight }"
>
<div <div
cdkDropList cdkDropList
#todoList="cdkDropList" style="outline: dashed 2px black;"
[cdkDropListData]="todo" (cdkDropListEntered)="onDropListEntered($event)"
[cdkDropListConnectedTo]="[doneList]" (cdkDropListDropped)="onDropListDropped()"
class="example-list" ></div>
(cdkDropListDropped)="drop($event)">
<div class="example-box" *ngFor="let item of todo" cdkDrag>{{item}}</div>
</div>
</div>
<div class="example-container">
<h2>Done</h2>
<div <div
cdkDropList cdkDropList
#doneList="cdkDropList" (cdkDropListEntered)="onDropListEntered($event)"
[cdkDropListData]="done" (cdkDropListDropped)="onDropListDropped()"
[cdkDropListConnectedTo]="[todoList]" *ngFor="let item of items"
class="example-list" >
(cdkDropListDropped)="drop($event)"> <div cdkDrag class="example-box" [ngClass]="{'example-box--wide': item%14==0}">{{ item }}</div>
<div class="example-box" *ngFor="let item of done" cdkDrag>{{item}}</div>
</div> </div>
</div> </div>

View File

@@ -8,8 +8,8 @@ describe('CustomizableGridComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ CustomizableGridComponent ] imports: [CustomizableGridComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(CustomizableGridComponent); fixture = TestBed.createComponent(CustomizableGridComponent);

View File

@@ -1,24 +1,123 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop'; import { CdkDragEnter, CdkDropList, moveItemInArray, DragRef, CdkDropListGroup, CdkDrag } from '@angular/cdk/drag-drop';
import { AfterViewInit } from '@angular/core';
import { ViewChild } from '@angular/core';
import { NgFor, NgStyle, NgClass } from '@angular/common';
@Component({ @Component({
selector: 'app-customizable-grid', selector: 'app-customizable-grid',
templateUrl: './customizable-grid.component.html' templateUrl: './customizable-grid.component.html',
standalone: true,
imports: [NgFor, CdkDropListGroup, NgStyle, CdkDropList, CdkDrag, NgClass]
}) })
export class CustomizableGridComponent { export class CustomizableGridComponent implements AfterViewInit {
todo = ['Get to work', 'Pick up groceries', 'Go home', 'Fall asleep']; @ViewChild(CdkDropList) placeholder!: CdkDropList;
done = ['Get up', 'Brush teeth', 'Take a shower', 'Check e-mail', 'Walk dog'];
drop(event: CdkDragDrop<string[]>) { private target: CdkDropList|null = null;
if (event.previousContainer === event.container) { private targetIndex: number = 0;
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); private source: CdkDropList|null = null;
} else { private sourceIndex: number = 0;
transferArrayItem( private dragRef: DragRef|null = null;
event.previousContainer.data,
event.container.data, items: Array<number> = [1, 2, 3, 4, 5, 6, 7, 8, 9];
event.previousIndex,
event.currentIndex, boxWidth = '200px';
); boxHeight = '200px';
ngAfterViewInit() {
const placeholderElement = this.placeholder.element.nativeElement;
placeholderElement.style.display = 'none';
placeholderElement.parentNode!.removeChild(placeholderElement);
}
add() {
this.items.push(this.items.length + 1);
}
shuffle() {
this.items.sort(function () {
return 0.5 - Math.random();
});
}
onDropListDropped() {
if (!this.target) {
return;
}
const placeholderElement: HTMLElement =
this.placeholder.element.nativeElement;
const placeholderParentElement: HTMLElement =
placeholderElement.parentElement!;
placeholderElement.style.display = 'none';
placeholderParentElement.removeChild(placeholderElement);
placeholderParentElement.appendChild(placeholderElement);
placeholderParentElement.insertBefore(
this.source!.element.nativeElement,
placeholderParentElement.children[this.sourceIndex]
);
if (this.placeholder._dropListRef.isDragging() && this.dragRef != null) {
this.placeholder._dropListRef.exit(this.dragRef);
}
this.target = null;
this.source = null;
this.dragRef = null;
if (this.sourceIndex !== this.targetIndex) {
moveItemInArray(this.items, this.sourceIndex, this.targetIndex);
} }
} }
onDropListEntered({ item, container }: CdkDragEnter) {
if (container == this.placeholder) {
return;
}
const placeholderElement: HTMLElement =
this.placeholder.element.nativeElement;
const sourceElement: HTMLElement = item.dropContainer.element.nativeElement;
const dropElement: HTMLElement = container.element.nativeElement;
const dragIndex: number = Array.prototype.indexOf.call(
dropElement.parentElement!.children,
this.source ? placeholderElement : sourceElement
);
const dropIndex: number = Array.prototype.indexOf.call(
dropElement.parentElement!.children,
dropElement
);
if (!this.source) {
this.sourceIndex = dragIndex;
this.source = item.dropContainer;
placeholderElement.style.width = this.boxWidth + 'px';
placeholderElement.style.height = this.boxHeight + 40 + 'px';
sourceElement.parentElement!.removeChild(sourceElement);
}
this.targetIndex = dropIndex;
this.target = container;
this.dragRef = item._dragRef;
placeholderElement.style.display = '';
placeholderElement.style.backgroundColor ='pink';
dropElement.parentElement!.insertBefore(
placeholderElement,
dropIndex > dragIndex ? dropElement.nextSibling : dropElement
);
this.placeholder._dropListRef.enter(
item._dragRef,
item.element.nativeElement.offsetLeft,
item.element.nativeElement.offsetTop
);
}
} }

View File

@@ -1,14 +1,8 @@
<style> <style>
:host { :host {
/*
height: calc(100% - 29px);
*/
width: 100%;
position: absolute;
padding: 0.5em; padding: 0.5em;
} }
.center { .center {
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -23,12 +17,15 @@
font-weight: bold; font-weight: bold;
color: #333; color: #333;
} }
.no-break {
white-space: nowrap;
}
</style> </style>
<div *ngIf="loading" class="center"> <div *ngIf="loading" class="center">
<mat-spinner></mat-spinner> <mat-spinner></mat-spinner>
</div> </div>
<div *ngIf="error" class="center is-error center-content "> <div *ngIf="error" class="center is-error center-content">
{{error}} {{error}}
</div> </div>
<div *ngIf="!loading && !error"> <div *ngIf="!loading && !error">
@@ -55,11 +52,14 @@
<th mat-header-cell *matHeaderCellDef>Description</th> <th mat-header-cell *matHeaderCellDef>Description</th>
<td mat-cell *matCellDef="let element">{{element.description}}</td> <td mat-cell *matCellDef="let element">{{element.description}}</td>
</ng-container> </ng-container>
<!-- Delete Column --> <!-- actions Column -->
<ng-container matColumnDef="delete"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element" class="no-break">
<button mat-icon-button (click)="delete(element)"> <button mat-icon-button (click)="edit(element)" aria-label="edit dashboard" title="edit dashboard">
<img src="assets/img/edit-outline.svg" class="icon-small" title="delete" />
</button>
<button mat-icon-button (click)="delete(element)" aria-label="delete dashboard" title="delete dashboard">
<img src="assets/img/recycle-bin-line.svg" class="icon-small" title="delete" /> <img src="assets/img/recycle-bin-line.svg" class="icon-small" title="delete" />
</button> </button>
</td> </td>

View File

@@ -8,8 +8,8 @@ describe('DashboardPageComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ DashboardPageComponent ] imports: [DashboardPageComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(DashboardPageComponent); fixture = TestBed.createComponent(DashboardPageComponent);

View File

@@ -1,24 +1,33 @@
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { Dashboard, DashboardCreationData, DashboardList, DashboardService } from '../dashboard.service'; import { Dashboard, DashboardCreationData, DashboardList, DashboardService } from '../dashboard.service';
import { NewDashboardComponent } from './new-dashboard/new-dashboard.component'; import { NewDashboardComponent } from './new-dashboard/new-dashboard.component';
import { NgIf } from '@angular/common';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatButton, MatIconButton } from '@angular/material/button';
import { MatTable, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow } from '@angular/material/table';
import { RouterLink } from '@angular/router';
@Component({ @Component({
selector: 'app-dashboard-page', selector: 'app-dashboard-page',
templateUrl: './dashboard-page.component.html' templateUrl: './dashboard-page.component.html',
standalone: true,
imports: [NgIf, MatProgressSpinner, MatButton, MatTable, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, RouterLink, MatIconButton, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow]
}) })
export class DashboardPageComponent implements OnInit { export class DashboardPageComponent implements OnInit {
displayedColumns: string[] = [/*'icon',*/ 'name', 'description','delete']; displayedColumns: string[] = [/*'icon',*/ 'name', 'description','actions'];
dataSource: Dashboard[] = []; dataSource: Dashboard[] = [];
loading = true; loading = true;
error = ""; error = "";
constructor(public dialog: MatDialog, private dashboardService: DashboardService){ constructor(
public dialog: MatDialog,
} private dashboardService: DashboardService,
private snackBar: MatSnackBar){}
ngOnInit(): void { ngOnInit(): void {
this.refreshTable(); this.refreshTable();
@@ -45,38 +54,57 @@ export class DashboardPageComponent implements OnInit {
createNewDashboard() { createNewDashboard() {
const dialogRef = this.dialog.open(NewDashboardComponent, { const dialogRef = this.dialog.open(NewDashboardComponent, {
data: {name: "", description: ""}, data: {name: "", description: ""},
hasBackdrop: true width: '30em'
}); });
dialogRef.afterClosed().subscribe((result: DashboardCreationData) => { dialogRef.afterClosed().subscribe((result: DashboardCreationData) => {
console.log('The dialog was closed with result ', JSON.stringify(result)); this.dashboardService.createDashboard(result).subscribe(result => this.refreshTable());
this.dashboardService.createDashboard(result).subscribe(result => { });
console.log(result); }
this.refreshTable(); edit(dashboard: Dashboard) {
}); const dialogRef = this.dialog.open(NewDashboardComponent, {
data: {name: dashboard.name, description: dashboard.description},
hasBackdrop: true
});
dialogRef.afterClosed().subscribe((result?: DashboardCreationData) => {
if (result) {
dashboard.name = result.name;
dashboard.description = result.description;
this.dashboardService.saveDashboard(dashboard).subscribe({
'error': () => {
this.snackBar.open("server made a boo boo", "", {
duration:5000
})
},
complete: () => {
this.refreshTable()
}
});
}
}); });
} }
delete(dashboard: Dashboard){ delete(dashboard: Dashboard){
const dialogRef = this.dialog.open(ConfirmationDialogComponent, { const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {title: "", text: "Delete dashboard '"+dashboard.name+"'?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}, data: {title: "", text: "Delete dashboard '"+dashboard.name+"'?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}
hasBackdrop: true
}); });
dialogRef.afterClosed().subscribe((result: boolean) => { dialogRef.afterClosed().subscribe((result: boolean) => {
if (result === true) { if (result === true) {
this.dashboardService.deleteDashboard(dashboard.id).subscribe({ this.dashboardService.deleteDashboard(dashboard.id).subscribe({
'error': (error) => { 'error': (error) => {
this.snackBar.open("failed to delete dashboard","", {
duration: 5000,
verticalPosition: 'top'
});
}, },
'complete': () => this.refreshTable() 'complete': () => this.refreshTable()
}); });
} }
}); });
} }
} }

View File

@@ -14,11 +14,9 @@
max-height: unset; max-height: unset;
} }
</style> </style>
<h1 mat-dialog-title>Add Plot</h1> <pdb-visualization-page mat-dialog-content #plot [defaultConfig]="data.config" [galleryEnabled]="false"></pdb-visualization-page>
<pdb-visualization-page mat-dialog-content #plot></pdb-visualization-page>
<div mat-dialog-actions align="end"> <div mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button> <button mat-button mat-dialog-close >Cancel</button>
<button class="save-button" mat-button mat-dialog-close (click)="onSaveClick()">Save</button> <button class="save-button" mat-flat-button mat-dialog-close (click)="onSaveClick()">Save</button>
</div> </div>

View File

@@ -8,8 +8,8 @@ describe('AddPlotDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ AddPlotDialogComponent ] imports: [AddPlotDialogComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(AddPlotDialogComponent); fixture = TestBed.createComponent(AddPlotDialogComponent);

View File

@@ -1,17 +1,27 @@
import { Component, ElementRef, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Inject, ViewChild } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialogContent, MatDialogActions, MatDialogClose } from '@angular/material/dialog';
import { PlotConfig } from 'src/app/plot.service';
import { VisualizationPageComponent } from 'src/app/visualization-page/visualization-page.component'; import { VisualizationPageComponent } from 'src/app/visualization-page/visualization-page.component';
import { VisualizationPageComponent as VisualizationPageComponent_1 } from '../../../visualization-page/visualization-page.component';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { MatButton } from '@angular/material/button';
@Component({ @Component({
selector: 'app-add-plot-dialog', selector: 'app-add-plot-dialog',
templateUrl: './add-plot-dialog.component.html', templateUrl: './add-plot-dialog.component.html',
styleUrls: ['./add-plot-dialog.component.scss'] standalone: true,
imports: [VisualizationPageComponent_1, CdkScrollable, MatDialogContent, MatDialogActions, MatButton, MatDialogClose]
}) })
export class AddPlotDialogComponent { export class AddPlotDialogComponent {
@ViewChild("plot") plotElement! :VisualizationPageComponent; @ViewChild("plot") plotElement! :VisualizationPageComponent;
constructor(public dialogRef: MatDialogRef<string>){
constructor(
public dialogRef: MatDialogRef<PlotConfig | undefined>,
@Inject(MAT_DIALOG_DATA) public data: {config: PlotConfig, title: string}
){
} }
onSaveClick(): void { onSaveClick(): void {

View File

@@ -1,16 +1,38 @@
<style> <style>
markdown {
--mdc-dialog-supporting-text-color: black;
}
.mat-mdc-dialog-content {
display: flex;
justify-content: center;
height: 22em;
}
.mat-mdc-dialog-content > div {
width:50%;
}
.preview {
margin-left: 0.5em;
overflow: auto;
}
mat-form-field textarea { mat-form-field textarea {
height: 7em; height: 20em;
} }
</style> </style>
<h1 mat-dialog-title>Add Text</h1> <h1 mat-dialog-title>Add Text</h1>
<div mat-dialog-content> <div mat-dialog-content>
<mat-form-field class="pdb-form-full-width"> <div>
<mat-label>Text</mat-label> <mat-form-field class="pdb-form-full-width">
<textarea matInput [(ngModel)]="text" #textElement focus></textarea> <mat-label>Text</mat-label>
</mat-form-field> <textarea matInput [(ngModel)]="text" #textElement focus ></textarea>
</mat-form-field>
<div>Text field supports <a href="https://spec.commonmark.org/" class="external-link" target="_blank" rel="noopener">Markdown</a>.</div>
</div>
<div class="preview">
<markdown [data]="this.text"></markdown>
</div>
</div> </div>
<div mat-dialog-actions align="end"> <div mat-dialog-actions align="end">
<button mat-button mat-dialog-close (click)="close()">Cancel</button> <button mat-button mat-dialog-close (click)="close()">Cancel</button>
<button class="save-button" mat-button mat-dialog-close (click)="onSaveClick()">Save</button> <button class="save-button" mat-flat-button mat-dialog-close (click)="onSaveClick()">Save</button>
</div> </div>

View File

@@ -8,8 +8,8 @@ describe('AddTextDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ AddTextDialogComponent ] imports: [AddTextDialogComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(AddTextDialogComponent); fixture = TestBed.createComponent(AddTextDialogComponent);

View File

@@ -1,9 +1,18 @@
import { Component, ElementRef, Inject, ViewChild } from '@angular/core'; import { Component, ElementRef, Inject, ViewChild } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose } from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
import { FocusDirective } from '../../../focus.directive';
import { MarkdownComponent } from 'ngx-markdown';
import { MatButton } from '@angular/material/button';
@Component({ @Component({
selector: 'app-add-text-dialog', selector: 'app-add-text-dialog',
templateUrl: './add-text-dialog.component.html' templateUrl: './add-text-dialog.component.html',
standalone: true,
imports: [MatDialogTitle, CdkScrollable, MatDialogContent, MatFormField, MatLabel, MatInput, FormsModule, FocusDirective, MarkdownComponent, MatDialogActions, MatButton, MatDialogClose]
}) })
export class AddTextDialogComponent { export class AddTextDialogComponent {
text = ""; text = "";
@@ -18,7 +27,6 @@ export class AddTextDialogComponent {
this.dialogRef.close(undefined); this.dialogRef.close(undefined);
} }
onSaveClick(): void { onSaveClick(): void {
this.dialogRef.close(this.text); this.dialogRef.close(this.text);
} }

View File

@@ -2,7 +2,16 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
}
.toolbar {
display: flex;
flex-direction: row;
}
.toolbar #filter-date-range{
flex-grow: 1;
display: flex;
align-items: center;
justify-content: flex-end;
} }
.center { .center {
position: absolute; position: absolute;
@@ -22,23 +31,46 @@
.content { .content {
padding: 0.5em; padding: 0.5em;
} }
.dashboard-area {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
}
.dashboard-column { .dashboard-column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* make all columns equal width - flex-basis:0 to make all resizing start from the same size*/
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
} }
.editable .editable-hovered { .editable {
padding: 0.5em;
}
.editable-hovered {
visibility: hidden; visibility: hidden;
} }
.editable:hover .editable-hovered {
.editable:hover .editable-hovered{
visibility: visible; visibility: visible;
} }
.handle { .handle {
display: block;
height: 1.5em; height: 1.5em;
visibility: hidden;
width: fit-content;
}
[cdkDrag] {
position: relative;
} }
[cdkDrag]:hover .handle, .cdk-drop-list-dragging .handle { [cdkDrag]:hover .handle, .cdk-drop-list-dragging .handle {
background-color: #f3f3f3;
border-radius: 3px;
cursor: grab; cursor: grab;
visibility: visible;
} }
</style> </style>
@@ -53,32 +85,33 @@
<div class="toolbar"> <div class="toolbar">
<button mat-button (click)="addText()">Add Text</button> <button mat-button (click)="addText()">Add Text</button>
<button mat-button (click)="addPlot()">Add Plot</button> <button mat-button (click)="addPlot()">Add Plot</button>
<button class="save-button" mat-button (click)="save()">Save</button> <button class="save-button" mat-flat-button (click)="save()" [disabled]="!isDirty()">Save</button>
<div id="filter-date-range">
Date range: <app-date-picker #datePicker (dateValueSelected)="updateDateRange($event)" ></app-date-picker>
</div>
</div> </div>
<div class="editable"> <div class="editable">
<h1>{{dashboard.name}}<button mat-icon-button (click)="editNameAndDescription()" class="editable-hovered"><img src="/assets/img/edit-outline.svg"/></button></h1> <h1>{{dashboard.name}}<button mat-icon-button (click)="editNameAndDescription()" class="editable-hovered"><img src="/assets/img/edit-outline.svg"/></button></h1>
<p>{{dashboard.description}}</p> <p>{{dashboard.description}}</p>
</div> </div>
<div cdkDropListGroup class="dashboard-area">
<div cdkDropListGroup>
<!-- All lists in here will be connected. -->
<div <div
cdkDropList cdkDropList
class="dashboard-column" class="dashboard-column"
*ngFor="let column of dashboard.arrangement" *ngFor="let i of [0,1]"
[cdkDropListData]="column" [cdkDropListData]="i"
(cdkDropListDropped)="drop($event)"> (cdkDropListDropped)="drop($event)">
<div <div
cdkDrag cdkDrag
*ngFor="let id of column" *ngFor="let id of dashboard.arrangement[i]"
[attr.widget-id]="id"> [attr.widget-id]="id">
<div cdkDragHandle class="handle"></div> <div cdkDragHandle class="handle"><img src="/assets/img/drag_handle.svg" class="icon-small"/></div>
<app-text-widget <app-text-widget
*ngIf="isTextWidget(id)" *ngIf="isTextWidget(id)"
[data]="getTextWidget(id)!"></app-text-widget> [data]="getTextWidget(id)!" (deleted)="delete($event)"></app-text-widget>
<app-plot-widget <app-plot-widget
*ngIf="isPlotWidget(id)" *ngIf="isPlotWidget(id)"
[data]="getPlotWidget(id)!"></app-plot-widget> [data]="getPlotWidget(id)!" (deleted)="delete($event)"></app-plot-widget>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,8 +8,8 @@ describe('DashboardComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ DashboardComponent ] imports: [DashboardComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(DashboardComponent); fixture = TestBed.createComponent(DashboardComponent);

View File

@@ -1,27 +1,41 @@
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; import { CdkDragDrop, moveItemInArray, transferArrayItem, CdkDropListGroup, CdkDropList, CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { Component, ElementRef, OnInit } from '@angular/core'; import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { BaseWidget, Dashboard, DashboardCreationData, DashboardService, PlotSize, PlotWidget, PlotWidgetRenderData, TextWidget } from 'src/app/dashboard.service'; import { Dashboard, DashboardCreationData, DashboardService, PlotWidget, PlotWidgetRenderData, TextWidget } from 'src/app/dashboard.service';
import { PlotConfig, PlotRequest, PlotResponse, PlotService, RenderOptions } from 'src/app/plot.service'; import { PlotConfig, PlotResponse, PlotService } from 'src/app/plot.service';
import { NewDashboardComponent } from '../new-dashboard/new-dashboard.component'; import { NewDashboardComponent } from '../new-dashboard/new-dashboard.component';
import { AddPlotDialogComponent } from './add-plot-dialog/add-plot-dialog.component'; import { AddPlotDialogComponent } from './add-plot-dialog/add-plot-dialog.component';
import { AddTextDialogComponent } from './add-text-dialog/add-text-dialog.component'; import { AddTextDialogComponent } from './add-text-dialog/add-text-dialog.component';
import { DatePickerChange, DatePickerComponent } from 'src/app/components/datepicker/date-picker.component';
import { NgIf, NgFor } from '@angular/common';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatButton, MatIconButton } from '@angular/material/button';
import { DatePickerComponent as DatePickerComponent_1 } from '../../components/datepicker/date-picker.component';
import { TextWidgetComponent } from './text-widget/text-widget.component';
import { PlotWidgetComponent } from './plot-widget/plot-widget.component';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
templateUrl: './dashboard.component.html' templateUrl: './dashboard.component.html',
standalone: true,
imports: [NgIf, MatProgressSpinner, RouterLink, MatButton, DatePickerComponent_1, MatIconButton, CdkDropListGroup, NgFor, CdkDropList, CdkDrag, CdkDragHandle, TextWidgetComponent, PlotWidgetComponent]
}) })
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit{
dashboard?: Dashboard = undefined; dashboard?: Dashboard = undefined;
pristineDashboardJSON?: string = undefined;
error = ""; error = "";
plotWidgetRenderData: PlotWidgetRenderData[] = []; plotWidgetRenderData: PlotWidgetRenderData[] = [];
@ViewChild("datePicker")
datePicker!: DatePickerComponent;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private service: DashboardService, private service: DashboardService,
@@ -34,10 +48,12 @@ export class DashboardComponent implements OnInit {
this.service.getDashboard(<string>this.route.snapshot.paramMap.get("id")).subscribe({ this.service.getDashboard(<string>this.route.snapshot.paramMap.get("id")).subscribe({
'next':(dashboard: Dashboard) => { 'next':(dashboard: Dashboard) => {
this.dashboard = dashboard; this.dashboard = dashboard;
this.pristineDashboardJSON = JSON.stringify(dashboard);
this.repairArrangement(); this.repairArrangement();
dashboard.plots.forEach(p => { dashboard.plots.forEach(p => {
this.plotWidgetRenderData.push(new PlotWidgetRenderData(p)); const submitterId = (<any>window).submitterId + (<any>window).randomId();
this.plotWidgetRenderData.push(new PlotWidgetRenderData(p, submitterId));
}); });
this.loadImages(0, this.plotWidgetRenderData); this.loadImages(0, this.plotWidgetRenderData);
@@ -54,68 +70,49 @@ export class DashboardComponent implements OnInit {
}); });
} }
updateDateRange(e: DatePickerChange) {
this.plotWidgetRenderData.forEach(r => {
r.widget.config.dateRange = e.value;
});
this.loadImages(0, this.plotWidgetRenderData);
}
isDirty() {
return this.pristineDashboardJSON !== JSON.stringify(this.dashboard);
}
loadImages(index: number, plotWidgetQueue: PlotWidgetRenderData[]) { loadImages(index: number, plotWidgetQueue: PlotWidgetRenderData[]) {
if (index < plotWidgetQueue.length){ if (index < plotWidgetQueue.length){
const plot = plotWidgetQueue[index]; const plot = plotWidgetQueue[index];
const request = this.createPlotRequest(plot.widget) if (plot.isAborted) {
this.loadImages(index +1 , plotWidgetQueue);
}else{
plot.plotResponse = undefined; // remove old image and show loading icon
const request = PlotWidget.createPlotRequest(plot.widget, plot.submitterId);
this.plotService.sendPlotRequest(request).subscribe({ this.plotService.sendPlotRequest(request).subscribe({
next: (response: PlotResponse)=> { next: (response: PlotResponse)=> {
plot.plotResponse= response; plot.plotResponse = response;
},
error: (error:any)=> {
plot.error = error;
this.loadImages(index +1 , plotWidgetQueue);
}, },
error: (error:any)=> {},
complete: () => { complete: () => {
this.loadImages(index +1 , plotWidgetQueue); this.loadImages(index +1 , plotWidgetQueue);
} }
}); });
}
}
createPlotRequest(plotWidget: PlotWidget): PlotRequest {
const height = this.height(plotWidget.size);
const width = this.width(plotWidget.size);
const fullWidth = window.innerWidth-30;
const fullHeight = window.innerHeight-30;
const request = new PlotRequest(
(<any>window).submitterId+crypto.randomUUID(),
plotWidget.config,
{
'main': new RenderOptions(height,width, false, true),
'fullScreen': new RenderOptions(fullHeight,fullWidth, false, true)
} }
);
return request;
}
height(size: PlotSize): number{
switch (size) {
case 'SMALL':
return 300;
case 'MEDIUM':
return 400;
case 'LARGE':
return 600;
}
}
width(size: PlotSize): number{
switch (size) {
case 'SMALL':
return 400;
case 'MEDIUM':
return 600;
case 'LARGE':
return 900;
} }
} }
private repairArrangement(){ private repairArrangement(){
const arrangement = this.dashboard!.arrangement || []; const arrangement = this.dashboard!.arrangement || [];
if (arrangement.length == 0){ for (let i = 0; i < 2; i++){
arrangement[0] = []; arrangement[i] = arrangement[i] ?? [] ;
} }
this.dashboard?.texts.forEach(t => { this.dashboard?.texts.forEach(t => {
if (!this.arrangmentContainsId(arrangement, t.id)){ if (!this.arrangmentContainsId(arrangement, t.id)){
@@ -127,6 +124,7 @@ export class DashboardComponent implements OnInit {
arrangement[0].push(t.id); arrangement[0].push(t.id);
} }
}); });
this.dashboard!.arrangement = arrangement; this.dashboard!.arrangement = arrangement;
} }
@@ -143,9 +141,9 @@ export class DashboardComponent implements OnInit {
addText() { addText() {
this.dialog.open(AddTextDialogComponent,{ this.dialog.open(AddTextDialogComponent,{
data: {text:""}, data: {text:""},
width: '600px' width: '800px'
}).afterClosed().subscribe((text: string) => { }).afterClosed().subscribe((text: string) => {
const widget = new TextWidget(crypto.randomUUID(),'MEDIUM', text); const widget = new TextWidget((<any>window).randomId(),'MEDIUM', text);
this.dashboard!.texts.push(widget); this.dashboard!.texts.push(widget);
this.dashboard!.arrangement[0].push(widget.id); this.dashboard!.arrangement[0].push(widget.id);
}); });
@@ -153,14 +151,15 @@ export class DashboardComponent implements OnInit {
addPlot() { addPlot() {
this.dialog.open(AddPlotDialogComponent,{ this.dialog.open(AddPlotDialogComponent,{
data: {},
width: 'calc(100% - 1em)', width: 'calc(100% - 1em)',
height: 'calc(100% - 1em)' height: 'calc(100% - 1em)'
}).afterClosed().subscribe((config: PlotConfig | "") => { }).afterClosed().subscribe((config: PlotConfig | "") => {
if (config != "" && config.query.length > 0) { if (config != "" && config.query.length > 0) {
const widget = new PlotWidget(crypto.randomUUID(), 'MEDIUM', config); const widget = new PlotWidget((<any>window).randomId(), 'MEDIUM', config);
this.dashboard!.plots.push(widget); this.dashboard!.plots.push(widget);
this.dashboard!.arrangement[0].push(widget.id); this.dashboard!.arrangement[0].push(widget.id);
this.plotWidgetRenderData.push(new PlotWidgetRenderData(widget)); this.plotWidgetRenderData.push(new PlotWidgetRenderData(widget, (<any>window).randomId()));
this.loadImages(this.plotWidgetRenderData.length-1, this.plotWidgetRenderData); this.loadImages(this.plotWidgetRenderData.length-1, this.plotWidgetRenderData);
} }
}); });
@@ -201,6 +200,7 @@ export class DashboardComponent implements OnInit {
duration: 5000, duration: 5000,
verticalPosition: 'top' verticalPosition: 'top'
}); });
this.pristineDashboardJSON = JSON.stringify(this.dashboard);
} }
}); });
} }
@@ -218,7 +218,6 @@ export class DashboardComponent implements OnInit {
this.dashboard!.description = result.description; this.dashboard!.description = result.description;
} }
}); });
} }
isTextWidget(id: string): boolean { isTextWidget(id: string): boolean {
@@ -237,16 +236,23 @@ export class DashboardComponent implements OnInit {
return this.plotWidgetRenderData.find( x => x.widget.id == id); return this.plotWidgetRenderData.find( x => x.widget.id == id);
} }
drop(event: CdkDragDrop<string[]>) { drop(event: CdkDragDrop<number>) {
if (event.previousContainer === event.container) { if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); moveItemInArray(this.dashboard!.arrangement[event.container.data], event.previousIndex, event.currentIndex);
} else { } else {
window.console.log("from ",event.previousContainer.data, " to ", event.container.data);
transferArrayItem( transferArrayItem(
event.previousContainer.data, this.dashboard!.arrangement[event.previousContainer.data],
event.container.data, this.dashboard!.arrangement[event.container.data],
event.previousIndex, event.previousIndex,
event.currentIndex, event.currentIndex,
); );
} }
} }
delete(dashboardId: string) {
this.dashboard!.arrangement[0] = this.dashboard!.arrangement[0].filter(a => a != dashboardId);
this.dashboard!.plots = this.dashboard!.plots.filter(p => p.id != dashboardId);
this.dashboard!.texts = this.dashboard!.texts.filter(t => t.id != dashboardId);
}
} }

View File

@@ -1,9 +1,12 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialogClose } from '@angular/material/dialog';
import { MatIconButton } from '@angular/material/button';
@Component({ @Component({
selector: 'app-full-screen-plot-dialog', selector: 'app-full-screen-plot-dialog',
templateUrl: './full-screen-plot-dialog.component.html' templateUrl: './full-screen-plot-dialog.component.html',
standalone: true,
imports: [MatIconButton, MatDialogClose]
}) })
export class FullScreenPlotDialogComponent { export class FullScreenPlotDialogComponent {

View File

@@ -3,7 +3,7 @@
float: left; float: left;
} }
.dashboard-card { .dashboard-card {
border: solid 1px red; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -16,15 +16,73 @@
width: 402px; width: 402px;
height: 302px; height: 302px;
} }
img { img.render-img {
cursor: zoom-in; cursor: zoom-in;
} }
.top-left {
position: absolute;
left: 0;
top: 0;
}
.top-right {
position: absolute;
right: 0;
top: 0;
}
.dashboard-card .editable-hovered {
visibility: hidden;
}
.dashboard-card:hover .editable-hovered {
visibility: visible;
}
.aborted-img {
flex-grow: 0.3;
opacity: 0.3;
}
.aborted-img img {
width: 100%;
}
.invader {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAUCAYAAACTQC2+AAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9kKGRAxBENShygAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAbUlEQVRIx9WVQQrAMAgEM6X///L2FCihYvRg7F416LCsQdKo0DWKZA4CBGzjev1lRHgezS0lkan3IYr485ZFdo5oJZkbWoRWfSWrJ8p6suvZucsgCS8THsHX+zKiO5uLaO763bpobvoS/e6HfQBzIE0PhAsDxgAAAABJRU5ErkJggg==);
width: 26px;
height: 20px;
display: inline-block;
vertical-align: text-bottom;
}
.spinner {
animation: wobble 2s linear infinite;
}
@keyframes wobble {
0% {
opacity: 1;
}
50% {
opacity: 0.3;
}
100% {
opacity: 1;
}
}
</style> </style>
<div class="dashboard-card" [ngClass]="{'size-medium' : true}"> <div class="dashboard-card" [ngClass]="{'size-medium' : true}">
<mat-spinner *ngIf="!hasRender('main') && !isError"></mat-spinner> <div class="editable-hovered top-right">
<img *ngIf="hasRender('main')" [src]="getImageUrl('main')" (click)="showFullScreenImage()" /> <button mat-icon-button (click)="edit()" ><img src="/assets/img/edit-outline.svg"/></button>
<div *ngIf="isError"> <button mat-icon-button (click)="delete()"><img src="/assets/img/recycle-bin-line.svg"/></button>
</div>
<div *ngIf="!hasRender('main') && !data?.error && !data.isAborted">
<button mat-button (click)="abort()"><span class="invader spinner"></span>Cancel</button>
</div>
<img *ngIf="hasRender('main')" [src]="getImageUrl('main')" (click)="showFullScreenImage()" class="render-img" />
<div *ngIf="data?.error && !data?.isAborted">
There was an error! This is a good time to panic! There was an error! This is a good time to panic!
</div> </div>
<div *ngIf="data?.isAborted" class="aborted-img">
<img src="assets/img/image-aborted.svg" />
</div>
</div> </div>

View File

@@ -8,8 +8,8 @@ describe('PlotWidgetComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ PlotWidgetComponent ] imports: [PlotWidgetComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(PlotWidgetComponent); fixture = TestBed.createComponent(PlotWidgetComponent);

View File

@@ -1,47 +1,98 @@
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; import { Component, EventEmitter, Input, Output, ViewChild, input } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ConfirmationDialogComponent } from 'src/app/confirmation-dialog/confirmation-dialog.component';
import { PlotWidget, PlotWidgetRenderData } from 'src/app/dashboard.service'; import { PlotWidget, PlotWidgetRenderData } from 'src/app/dashboard.service';
import { PlotViewComponent } from 'src/app/plot-view/plot-view.component'; import { PlotViewComponent } from 'src/app/plot-view/plot-view.component';
import { PlotRequest, PlotResponse, PlotService, RenderOptions } from 'src/app/plot.service'; import { PlotConfig, PlotResponse, PlotService } from 'src/app/plot.service';
import { AddPlotDialogComponent } from '../add-plot-dialog/add-plot-dialog.component';
import { FullScreenPlotDialogComponent } from '../full-screen-plot-dialog/full-screen-plot-dialog.component'; import { FullScreenPlotDialogComponent } from '../full-screen-plot-dialog/full-screen-plot-dialog.component';
import { NgClass, NgIf } from '@angular/common';
import { MatIconButton, MatButton } from '@angular/material/button';
@Component({ @Component({
selector: 'app-plot-widget', selector: 'app-plot-widget',
templateUrl: './plot-widget.component.html' templateUrl: './plot-widget.component.html',
standalone: true,
imports: [NgClass, MatIconButton, NgIf, MatButton]
}) })
export class PlotWidgetComponent implements AfterViewInit { export class PlotWidgetComponent {
@Input("data") @Input("data")
data!: PlotWidgetRenderData; data!: PlotWidgetRenderData;
public thumbnailUrl = ""; public thumbnailUrl = "";
isError = false;
@ViewChild("plotView") plotView!: PlotViewComponent; //@ViewChild("plotView") plotView!: PlotViewComponent;
constructor(private dialog: MatDialog, ){} @Output()
deleted : EventEmitter<string> = new EventEmitter<string>();
ngAfterViewInit(): void { constructor(private dialog: MatDialog, private service: PlotService){}
}
hasRender(name: string): boolean{ hasRender(name: string): boolean{
const hasRender = this.data !== undefined && this.data.plotResponse !== undefined && this.data.plotResponse?.rendered[name] !== undefined; return this.data !== undefined && this.data.plotResponse !== undefined && this.data.plotResponse?.rendered[name] !== undefined;
return hasRender;
} }
getImageUrl(name: string ): string | undefined { getImageUrl(name: string ): string | undefined {
return this.data?.plotResponse?.rendered[name]; return this.data?.plotResponse?.rendered[name];
} }
showFullScreenImage(){ showFullScreenImage() {
this.dialog.open(FullScreenPlotDialogComponent,{ this.dialog.open(FullScreenPlotDialogComponent,{
width: 'calc(100% - 15px)', width: 'calc(100% - 15px)',
height: 'calc(100% - 15px)', height: 'calc(100% - 15px)',
'data': {'imageUrl': this.getImageUrl('fullScreen')} data: {'imageUrl': this.getImageUrl('fullScreen')}
}).afterClosed().subscribe(() => { }).afterClosed().subscribe(() => {
}); });
} }
abort(){
window.console.log("abort");
this.data.isAborted = true;
this.service.abort(this.data.submitterId).subscribe({
complete: () => {
window.console.log("cancelled");
},
error: () => {}
});
}
delete() {
this.dialog
.open(ConfirmationDialogComponent, {
data: {title: "", text: "Delete plot?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}
})
.afterClosed()
.subscribe((result: boolean) => {
if (result === true) {
this.deleted.emit(this.data.widget.id);
}
});
}
edit() {
this.dialog.open(AddPlotDialogComponent, {
data: {config: this.data.widget.config},
width: 'calc(100% - 15px)',
height: 'calc(100% - 15px)',
}).afterClosed().subscribe((config?: PlotConfig) => {
if (config !== undefined && config.query.length > 0) {
this.data.widget.config = config;
this.data.error = false;
this.data.plotResponse = undefined;
const request = PlotWidget.createPlotRequest(this.data.widget, this.data.submitterId);
this.service.sendPlotRequest(request).subscribe({
next: (response: PlotResponse)=> {
this.data.plotResponse = response;
},
error: (error:any)=> {
this.data.error = true;
},
});
}
});
}
} }

View File

@@ -4,7 +4,7 @@
} }
.text-widget { .text-widget {
position: relative; position: relative;
padding: 1em 0; padding-bottom: 1em;
} }
.text-widget:hover { .text-widget:hover {
/*outline: solid 1px black;/**/ /*outline: solid 1px black;/**/
@@ -14,7 +14,7 @@
.editable-hovered { .editable-hovered {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: -2em;
} }
.text-widget .editable-hovered { .text-widget .editable-hovered {
@@ -25,6 +25,9 @@
} }
</style> </style>
<div class="text-widget"> <div class="text-widget">
<button mat-icon-button (click)="edit()" class="editable-hovered"><img src="/assets/img/edit-outline.svg"/></button> <div class="editable-hovered">
<p *ngFor="let line of lines()">{{line}}</p> <button mat-icon-button (click)="edit()"><img src="/assets/img/edit-outline.svg"/></button>
<button mat-icon-button (click)="delete()"><img src="/assets/img/recycle-bin-line.svg"/></button>
</div>
<markdown [data]="this.data.text"></markdown>
</div> </div>

View File

@@ -8,8 +8,8 @@ describe('TextWidgetComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ TextWidgetComponent ] imports: [TextWidgetComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(TextWidgetComponent); fixture = TestBed.createComponent(TextWidgetComponent);

View File

@@ -1,26 +1,46 @@
import { Component, Input } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ConfirmationDialogComponent } from 'src/app/confirmation-dialog/confirmation-dialog.component';
import { TextWidget } from 'src/app/dashboard.service'; import { TextWidget } from 'src/app/dashboard.service';
import { AddTextDialogComponent } from '../add-text-dialog/add-text-dialog.component'; import { AddTextDialogComponent } from '../add-text-dialog/add-text-dialog.component';
import { MatIconButton } from '@angular/material/button';
import { MarkdownComponent } from 'ngx-markdown';
@Component({ @Component({
selector: 'app-text-widget', selector: 'app-text-widget',
templateUrl: './text-widget.component.html' templateUrl: './text-widget.component.html',
standalone: true,
imports: [MatIconButton, MarkdownComponent]
}) })
export class TextWidgetComponent { export class TextWidgetComponent {
@Input() @Input()
data! : TextWidget; data! : TextWidget;
@Output()
deleted : EventEmitter<string> = new EventEmitter<string>();
constructor(private dialog: MatDialog){} constructor(private dialog: MatDialog){}
lines(): string[]{ lines(): string[]{
return typeof this.data.text == 'string' ? this.data.text.split(/\r?\n/) : []; return typeof this.data.text == 'string' ? this.data.text.split(/\r?\n/) : [];
} }
delete() {
this.dialog
.open(ConfirmationDialogComponent, {
data: {title: "", text: "Delete text?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}
})
.afterClosed()
.subscribe((result: boolean) => {
if (result === true) {
this.deleted.emit(this.data.id);
}
});
}
edit() { edit() {
this.dialog.open(AddTextDialogComponent,{ this.dialog.open(AddTextDialogComponent,{
data: {text : this.data.text}, data: {text : this.data.text},
width: '600px' width: '800px'
}).afterClosed().subscribe((text?: string) => { }).afterClosed().subscribe((text?: string) => {
if (text !== undefined) { if (text !== undefined) {
this.data.text = text; this.data.text = text;

View File

@@ -1,15 +1,29 @@
<h1 mat-dialog-title>Create a new dashboard</h1> <style>
<div mat-dialog-content> div[mat-dialog-content] {
<mat-form-field class="pdb-form-full-width"> overflow: hidden;
<mat-label>Name</mat-label> }
<input matInput [(ngModel)]="data.name" #name> </style>
</mat-form-field> <form [formGroup]="registerForm" >
<mat-form-field class="pdb-form-full-width"> <h1 mat-dialog-title>Create a new dashboard</h1>
<mat-label>Description</mat-label> <div mat-dialog-content>
<textarea matInput [(ngModel)]="data.description"></textarea> <mat-form-field class="pdb-form-full-width">
</mat-form-field> <mat-label>Name</mat-label>
</div> <input matInput [(ngModel)]="data.name" #name formControlName="name" focus maxlength="64" required="required" />
<div mat-dialog-actions align="end"> <mat-error>Name must be between one and 64 characters.</mat-error>
<button mat-button mat-dialog-close>Cancel</button> </mat-form-field>
<button class="save-button" mat-button mat-dialog-close (click)="onSaveClick()" cdkFocusInitial>Save</button> <mat-form-field class="pdb-form-full-width">
</div> <mat-label>Description</mat-label>
<textarea matInput [(ngModel)]="data.description" maxlength="65535" formControlName="description"></textarea>
</mat-form-field>
</div>
<div mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
class="save-button"
mat-button
mat-dialog-close
(click)="onSaveClick()"
[disabled]="!registerForm.valid">Save</button>
</div>
</form>

View File

@@ -1,4 +0,0 @@
div[mat-dialog-content] {
overflow: hidden;
}

View File

@@ -8,8 +8,8 @@ describe('NewDashboardComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ NewDashboardComponent ] imports: [NewDashboardComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(NewDashboardComponent); fixture = TestBed.createComponent(NewDashboardComponent);

View File

@@ -1,14 +1,27 @@
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import {MatDialog, MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose } from '@angular/material/dialog';
import { DashboardCreationData } from 'src/app/dashboard.service'; import { DashboardCreationData } from 'src/app/dashboard.service';
import { AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { MatFormField, MatLabel, MatError } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { FocusDirective } from '../../focus.directive';
import { MatButton } from '@angular/material/button';
@Component({ @Component({
selector: 'app-new-dashboard', selector: 'app-new-dashboard',
templateUrl: './new-dashboard.component.html', templateUrl: './new-dashboard.component.html',
styleUrls: ['./new-dashboard.component.scss'] standalone: true,
imports: [FormsModule, ReactiveFormsModule, MatDialogTitle, CdkScrollable, MatDialogContent, MatFormField, MatLabel, MatInput, FocusDirective, MatError, MatDialogActions, MatButton, MatDialogClose]
}) })
export class NewDashboardComponent implements OnInit { export class NewDashboardComponent implements OnInit {
registerForm = new FormGroup({
name: new FormControl('', [Validators.pattern(/^[^\s]+.{0,63}$/), Validators.required]),
description: new FormControl('', [Validators.maxLength(65535)]),
});
@ViewChild('name') nameInput!: ElementRef; @ViewChild('name') nameInput!: ElementRef;
constructor(public dialogRef: MatDialogRef<NewDashboardComponent>, constructor(public dialogRef: MatDialogRef<NewDashboardComponent>,
@@ -16,7 +29,7 @@ export class NewDashboardComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
window.setTimeout(() => this.nameInput.nativeElement.focus(), 0); //window.setTimeout(() => this.nameInput.nativeElement.focus(), 0);
} }
onSaveClick(): void { onSaveClick(): void {

View File

@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { PlotConfig, PlotResponse } from './plot.service'; import { PlotConfig, PlotRequest, PlotResponse, RenderOptions } from './plot.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -67,6 +67,47 @@ export class PlotWidget extends BaseWidget {
constructor(override id: string, override size: 'SMALL'|'MEDIUM'|'LARGE', public config: PlotConfig) { constructor(override id: string, override size: 'SMALL'|'MEDIUM'|'LARGE', public config: PlotConfig) {
super(id, 'PLOT', size); super(id, 'PLOT', size);
} }
public static createPlotRequest(widget: PlotWidget, submitterId: string): PlotRequest {
const height = this.height(widget.size);
const width = this.width(widget.size);
const fullWidth = window.innerWidth-30;
const fullHeight = window.innerHeight-30;
const request = new PlotRequest(
submitterId,
widget.config,
{
'main': new RenderOptions(height,width, false, true),
'fullScreen': new RenderOptions(fullHeight,fullWidth, false, true)
}
);
return request;
}
static height(size: PlotSize): number{
switch (size) {
case 'SMALL':
return 300;
case 'MEDIUM':
return 400;
case 'LARGE':
return 600;
}
}
static width(size: PlotSize): number{
switch (size) {
case 'SMALL':
return 400;
case 'MEDIUM':
return 600;
case 'LARGE':
return 900;
}
}
} }
export type PlotSize = 'SMALL'|'MEDIUM'|'LARGE'; export type PlotSize = 'SMALL'|'MEDIUM'|'LARGE';
@@ -74,7 +115,9 @@ export type PlotSize = 'SMALL'|'MEDIUM'|'LARGE';
export type PlotType = 'TEXT'|'PLOT'; export type PlotType = 'TEXT'|'PLOT';
export class PlotWidgetRenderData { export class PlotWidgetRenderData {
constructor(public widget: PlotWidget, public plotResponse?: PlotResponse) { public isAborted = false;
public error: string|boolean = false;
constructor(public widget: PlotWidget, public submitterId: string, public plotResponse?: PlotResponse) {
} }
} }

View File

@@ -1,7 +1,8 @@
import { AfterViewInit, Directive, ElementRef } from '@angular/core'; import { AfterViewInit, Directive, ElementRef } from '@angular/core';
@Directive({ @Directive({
selector: '[focus]' selector: '[focus]',
standalone: true
}) })
export class FocusDirective implements AfterViewInit { export class FocusDirective implements AfterViewInit {

View File

@@ -8,8 +8,8 @@ describe('GalleryViewComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ GalleryViewComponent ] imports: [GalleryViewComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,7 +1,19 @@
import { Component, OnInit, Input, Output, ViewChild, EventEmitter } from '@angular/core'; import { Component, OnInit, Input, Output, ViewChild, EventEmitter, forwardRef } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { PlotService, PlotRequest, PlotResponse, PlotResponseStats, DashTypeAndColor, RenderedImages } from '../plot.service'; import { PlotService, PlotRequest, PlotResponse, PlotResponseStats, DashTypeAndColor, RenderedImages } from '../plot.service';
import { UtilService } from '../utils.service'; import { UtilService } from '../utils.service';
import { NgIf, NgClass, NgFor } from '@angular/common';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatSelect } from '@angular/material/select';
import { MatOption } from '@angular/material/core';
import { ImageToggleComponent } from '../image-toggle/image-toggle.component';
import { MatCheckbox } from '@angular/material/checkbox';
import { FormsModule } from '@angular/forms';
import { MatProgressBar } from '@angular/material/progress-bar';
import { MatButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
import { PlotDetailsComponent } from '../plot-details/plot-details.component';
import { MatInput } from '@angular/material/input';
export class GalleryFilterData { export class GalleryFilterData {
filterBy :string; filterBy :string;
@@ -17,9 +29,11 @@ export class GalleryFilterData {
} }
@Component({ @Component({
selector: 'pdb-gallery-filter-view', selector: 'pdb-gallery-filter-view',
templateUrl: './gallery-filter-view.component.html', templateUrl: './gallery-filter-view.component.html',
styleUrls: ['./gallery-filter-view.component.scss'] styleUrls: ['./gallery-filter-view.component.scss'],
standalone: true,
imports: [MatFormField, MatLabel, MatSelect, MatOption, NgIf, ImageToggleComponent, MatInput, FormsModule]
}) })
export class GalleryFilterView { export class GalleryFilterView {
compareImages = JSON.stringify([ compareImages = JSON.stringify([
@@ -89,9 +103,11 @@ export class GalleryFilterView {
@Component({ @Component({
selector: 'pdb-gallery-view', selector: 'pdb-gallery-view',
templateUrl: './gallery-view.component.html', templateUrl: './gallery-view.component.html',
styleUrls: ['./gallery-view.component.scss'] styleUrls: ['./gallery-view.component.scss'],
standalone: true,
imports: [NgIf, MatFormField, MatLabel, MatSelect, MatOption, ImageToggleComponent, GalleryFilterView, MatCheckbox, FormsModule, MatProgressBar, MatButton, MatTooltip, NgClass, NgFor, forwardRef(() => GalleryItemView)]
}) })
export class GalleryViewComponent implements OnInit { export class GalleryViewComponent implements OnInit {
@@ -313,9 +329,11 @@ export class GalleryViewComponent implements OnInit {
} }
@Component({ @Component({
selector: 'pdb-gallery-item-view', selector: 'pdb-gallery-item-view',
templateUrl: './gallery-item-view.component.html', templateUrl: './gallery-item-view.component.html',
styleUrls: ['./gallery-item-view.component.scss'] styleUrls: ['./gallery-item-view.component.scss'],
standalone: true,
imports: [NgClass, NgIf, PlotDetailsComponent]
}) })
export class GalleryItemView { export class GalleryItemView {
@Input() @Input()

View File

@@ -8,8 +8,8 @@ describe('HelpPageComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ HelpPageComponent ] imports: [HelpPageComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'pdb-help-page', selector: 'pdb-help-page',
templateUrl: './help-page.component.html', templateUrl: './help-page.component.html',
styleUrls: ['./help-page.component.scss'] styleUrls: ['./help-page.component.scss'],
standalone: true
}) })
export class HelpPageComponent implements OnInit { export class HelpPageComponent implements OnInit {

View File

@@ -8,8 +8,8 @@ describe('ImageToggleComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ ImageToggleComponent ] imports: [ImageToggleComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,9 +1,12 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { NgIf } from '@angular/common';
@Component({ @Component({
selector: 'pdb-image-toggle', selector: 'pdb-image-toggle',
templateUrl: './image-toggle.component.html', templateUrl: './image-toggle.component.html',
styleUrls: ['./image-toggle.component.scss'] styleUrls: ['./image-toggle.component.scss'],
standalone: true,
imports: [NgIf]
}) })
export class ImageToggleComponent implements OnInit { export class ImageToggleComponent implements OnInit {

View File

@@ -17,6 +17,7 @@
<mat-form-field class="pdb-form-number" <mat-form-field class="pdb-form-number"
*ngIf="limitBy !== 'NO_LIMIT'"> *ngIf="limitBy !== 'NO_LIMIT'">
<mat-label><!--empty label needed for layout reasons--></mat-label>
<input <input
matInput matInput
type="number" type="number"

View File

@@ -8,8 +8,8 @@ describe('LimitByComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ LimitByComponent ] imports: [LimitByComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,10 +1,17 @@
import { Component, Input} from '@angular/core'; import { Component, Input} from '@angular/core';
import {FormControl} from '@angular/forms'; import { FormControl, FormsModule } from '@angular/forms';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatSelect } from '@angular/material/select';
import { MatOption } from '@angular/material/core';
import { NgIf } from '@angular/common';
import { MatInput } from '@angular/material/input';
@Component({ @Component({
selector: 'pdb-limit-by', selector: 'pdb-limit-by',
templateUrl: './limit-by.component.html', templateUrl: './limit-by.component.html',
styleUrls: ['./limit-by.component.scss'] styleUrls: ['./limit-by.component.scss'],
standalone: true,
imports: [MatFormField, MatLabel, MatSelect, MatOption, NgIf, MatInput, FormsModule]
}) })
export class LimitByComponent { export class LimitByComponent {

View File

@@ -8,8 +8,8 @@ describe('MainPageComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ MainPageComponent ] imports: [MainPageComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,9 +1,12 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({ @Component({
selector: 'pdb-main-page', selector: 'pdb-main-page',
templateUrl: './main-page.component.html', templateUrl: './main-page.component.html',
styleUrls: ['./main-page.component.scss'] styleUrls: ['./main-page.component.scss'],
standalone: true,
imports: [RouterLink]
}) })
export class MainPageComponent implements OnInit { export class MainPageComponent implements OnInit {

View File

@@ -1,33 +1,4 @@
.plot-details-plotType {
background-image: url(/assets/img/pointTypes.png);
width: 9px;
height: 7px;
transform: scale(1.5);
}
.plot-details-plotType_0 {background-position-x: 0px;}
.plot-details-plotType_1 {background-position-x: -10px;}
.plot-details-plotType_2 {background-position-x: -20px;}
.plot-details-plotType_3 {background-position-x: -30px;}
.plot-details-plotType_4 {background-position-x: -40px;}
.plot-details-plotType_5 {background-position-x: -50px;}
.plot-details-plotType_6 {background-position-x: -60px;}
.plot-details-plotType_7 {background-position-x: -70px;}
.plot-details-plotType_8 {background-position-x: -80px;}
.plot-details-plotType_9 {background-position-x: -90px;}
.plot-details-plotType_10 {background-position-x:-100px;}
.plot-details-plotType_11 {background-position-x:-110px;}
.plot-details-plotType_12 {background-position-x:-120px;}
.plot-details-plotType_0051c2 {background-position-y: 0px;}
.plot-details-plotType_bf8300 {background-position-y: -8px;}
.plot-details-plotType_9400d3 {background-position-y: -16px;}
.plot-details-plotType_00c254 {background-position-y: -24px;}
.plot-details-plotType_e6e600 {background-position-y: -32px;}
.plot-details-plotType_e51e10 {background-position-y: -40px;}
.plot-details-plotType_57a1c2 {background-position-y: -48px;}
.plot-details-plotType_bd36c2 {background-position-y: -56px;}
.gallery-item-details td { .gallery-item-details td {

View File

@@ -1,11 +1,16 @@
import { Component, OnInit, Input, Output, ViewChild, EventEmitter, ɵpublishDefaultGlobalUtils } from '@angular/core'; import { Component, Input } from '@angular/core';
import { DashTypeAndColor, PlotResponseStats, DataSeriesStats } from '../plot.service'; import { DashTypeAndColor, PlotResponseStats, DataSeriesStats } from '../plot.service';
import { UtilService } from '../utils.service'; import { UtilService } from '../utils.service';
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
import { FormsModule } from '@angular/forms';
import { NgFor, NgIf } from '@angular/common';
@Component({ @Component({
selector: 'pdb-plot-details', selector: 'pdb-plot-details',
templateUrl: './plot-details.component.html', templateUrl: './plot-details.component.html',
styleUrls: ['./plot-details.component.scss'] styleUrls: ['./plot-details.component.scss'],
standalone: true,
imports: [MatRadioGroup, FormsModule, MatRadioButton, NgFor, NgIf]
}) })
export class PlotDetailsComponent { export class PlotDetailsComponent {

View File

@@ -1,3 +1,16 @@
<!---->
<div cdkDrag
[ngClass]="{'hidden': !imageUrl || showStats || dataSeries().length == 0}"
class="plot-view--legend"
[cdkDragFreeDragPosition]="legendInitialPosition">
<div cdkDragHandle></div>
<div class="plot-view--legend-content">
<ol>
<li *ngFor="let stat of dataSeries()"><div class="{{ pointTypeClass(stat.dashTypeAndColor) }}" title="{{ stat.name }}"></div>{{ stat.name }}</li>
</ol>
</div>
</div>
<div <div
*ngIf="imageUrl"> *ngIf="imageUrl">
<div <div

View File

@@ -20,4 +20,61 @@ img {
background-color: white; background-color: white;
box-shadow: 5px 5px 10px 0px #e0e0e0; box-shadow: 5px 5px 10px 0px #e0e0e0;
overflow: auto; overflow: auto;
}
.plot-view--legend {
border: solid 1px #ccc;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 4px;
position: fixed;
z-index: 1;
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
background-color: white;
}
.plot-view--legend:active {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.plot-view--legend div[cdkDragHandle] {
visibility: hidden;
height: 1.2rem;
flex-shrink: 0;
}
.plot-view--legend:hover div[cdkDragHandle] {
cursor: move;
visibility: visible;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAF0lEQVQI12P4//8/AwMDhGSEUFCAUwYAJl4R8Z1D4wIAAAAASUVORK5CYII=);
}
.plot-view--legend-content {
max-height: 30em;
overflow: auto;
max-width: 60em;
overflow: auto;
resize: both;
padding-bottom: 0.5em;
}
.plot-view--legend ol {
padding-inline-start: 0.7em;
}
.plot-view--legend ol li {
list-style-type: none;
display: flex;
flex-direction: row;
align-items: center
}
.plot-view--legend ol li .plot-details-plotType{
margin-right: 0.3em;
flex-shrink: 0;
flex-grow: 0;
} }

View File

@@ -8,8 +8,8 @@ describe('PlotViewComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ PlotViewComponent ] imports: [PlotViewComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,23 +1,34 @@
import { Component, OnInit, Output, EventEmitter } from '@angular/core'; import { Component, Output, EventEmitter } from '@angular/core';
import { DataType, AxesTypes, PlotResponseStats, PlotConfig, PlotService, PlotResponse, PlotRequest, RenderOptions } from '../plot.service'; import { DataType, AxesTypes, PlotResponseStats, PlotConfig, PlotService, PlotResponse, PlotRequest, RenderOptions, DataSeriesStats, DashTypeAndColor } from '../plot.service';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import * as moment from 'moment'; //import * as moment from 'moment';
import { WidgetDimensions } from '../dashboard.service'; import { WidgetDimensions } from '../dashboard.service';
import { Overlay } from "@angular/cdk/overlay";
import { DateTime, Duration } from "luxon";
import { DateValue } from '../components/datepicker/date-picker.component';
import { Observable } from 'rxjs';
import { CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
import { NgClass, NgFor, NgIf } from '@angular/common';
import { PlotDetailsComponent } from '../plot-details/plot-details.component';
@Component({ @Component({
selector: 'pdb-plot-view', selector: 'pdb-plot-view',
templateUrl: './plot-view.component.html', templateUrl: './plot-view.component.html',
styleUrls: ['./plot-view.component.scss'] styleUrls: ['./plot-view.component.scss'],
standalone: true,
imports: [CdkDrag, NgClass, CdkDragHandle, NgFor, NgIf, PlotDetailsComponent]
}) })
export class PlotViewComponent implements OnInit { export class PlotViewComponent {
readonly DATE_PATTERN = "YYYY-MM-DD HH:mm:ss"; // for moment-JS readonly DATE_PATTERN = "yyyy-MM-dd HH:mm:ss"; // for moment-JS
readonly gnuplotLMargin = 110; // The left margin configured for gnuplot readonly gnuplotLMargin = 110; // The left margin configured for gnuplot
readonly gnuplotRMargin = 110; // The right margin configured for gnuplot readonly gnuplotRMargin = 110; // The right margin configured for gnuplot
readonly gnuplotTMargin = 57; // The top margin configured for gnuplot readonly gnuplotTMargin = 57; // The top margin configured for gnuplot
readonly gnuplotBMargin = 76; // The bottom margin configured for gnuplot readonly gnuplotBMargin = 76; // The bottom margin configured for gnuplot
isOpen = false;
imageUrl! : string; imageUrl! : string;
stats: PlotResponseStats | null = null; stats: PlotResponseStats | null = null;
@@ -28,7 +39,7 @@ export class PlotViewComponent implements OnInit {
loadingEvent : EventEmitter<LoadingEvent> = new EventEmitter<LoadingEvent>(); loadingEvent : EventEmitter<LoadingEvent> = new EventEmitter<LoadingEvent>();
@Output() @Output()
dateRangeUpdateEvent : EventEmitter<string> = new EventEmitter<string>(); dateRangeUpdateEvent : EventEmitter<DateValue> = new EventEmitter<DateValue>();
in_drag_mode = false; in_drag_mode = false;
drag_start_x = 0; drag_start_x = 0;
@@ -47,10 +58,9 @@ export class PlotViewComponent implements OnInit {
config? : PlotConfig; config? : PlotConfig;
constructor(private service : PlotService, private snackBar: MatSnackBar) { } legendInitialPosition = {x:115,y:60};
ngOnInit() { constructor(private service : PlotService, private snackBar: MatSnackBar, private overlay: Overlay) { }
}
showError(message:string) { showError(message:string) {
@@ -177,14 +187,12 @@ export class PlotViewComponent implements OnInit {
} }
setDateRange(startDate: any, endDate: any) { setDateRange(startDate: any, endDate: any) {
const formattedStartDate = startDate.format(this.DATE_PATTERN); const formattedStartDate = startDate.toFormat(this.DATE_PATTERN);
const formattedEndDate = endDate.format(this.DATE_PATTERN); const formattedEndDate = endDate.toFormat(this.DATE_PATTERN);
const newDateRange = formattedStartDate+" - "+formattedEndDate; const newDateRange = formattedStartDate+" - "+formattedEndDate;
const newDateValue = new DateValue('ABSOLUTE', newDateRange, newDateRange);
//(<HTMLInputElement>document.getElementById("search-date-range")).value = newDateRange; this.dateRangeUpdateEvent.emit(newDateValue);
this.dateRangeUpdateEvent.emit(newDateRange);
//this.plot();
} }
zoomRange(range: SelectionRange) { zoomRange(range: SelectionRange) {
@@ -250,7 +258,11 @@ export class PlotViewComponent implements OnInit {
this.axes = this.getAxes(); this.axes = this.getAxes();
const request = this.createPlotRequest(dimension); const request = this.createPlotRequest(dimension);
this.imageUrl = '';
this.stats = null;
document.dispatchEvent(new Event("invadersStart", {}));
this.loadingEvent.emit(new LoadingEvent(true)); this.loadingEvent.emit(new LoadingEvent(true));
const x = this.service.sendPlotRequest(request).subscribe({ const x = this.service.sendPlotRequest(request).subscribe({
next: (plotResponse: PlotResponse) => { next: (plotResponse: PlotResponse) => {
@@ -281,7 +293,7 @@ export class PlotViewComponent implements OnInit {
'main': new RenderOptions(actualDimension.height, actualDimension.width, false, true) 'main': new RenderOptions(actualDimension.height, actualDimension.width, false, true)
}); });
return request; return request;
} }
/** /**
* Zoom in/out by zoomFaktor, so that the anchorInPercentOfDateRange keeps the same position. * Zoom in/out by zoomFaktor, so that the anchorInPercentOfDateRange keeps the same position.
@@ -289,18 +301,26 @@ export class PlotViewComponent implements OnInit {
* shiftDateByAnchor(dateRangeAsString, 0.20, 0.5) zooms in by 50%, so that the date that was at 20% before the zoom is still at 20% after the zoom * shiftDateByAnchor(dateRangeAsString, 0.20, 0.5) zooms in by 50%, so that the date that was at 20% before the zoom is still at 20% after the zoom
* shiftDateByAnchor(dateRangeAsString, 0.33, 2) zooms out by 50%, so that the date that was at 33% before the zoom is still at 33% after the zoom * shiftDateByAnchor(dateRangeAsString, 0.33, 2) zooms out by 50%, so that the date that was at 33% before the zoom is still at 33% after the zoom
*/ */
shiftDateByAnchor(dateRange:string, anchorInPercentOfDateRange:number, zoomFactor:number) shiftDateByAnchor(dateValue:DateValue, anchorInPercentOfDateRange:number, zoomFactor:number)
{ {
const dateRangeParsed = this.parseDateRange(dateRange); const dateRangeParsed = this.parseDateRange(dateValue);
const dateRangeInSeconds = dateRangeParsed.duration.asSeconds(); dateRangeParsed.subscribe({
next: (dataRange: DateRange) => {
const dateRangeInSeconds = Math.floor(dataRange.duration.toMillis()/1000);
const anchorTimestampInSeconds = dateRangeParsed.startDate.clone().add(Math.floor(dateRangeInSeconds*anchorInPercentOfDateRange), "seconds"); const anchorTimestampInSeconds = dataRange.startDate.plus(Math.floor(dateRangeInSeconds*anchorInPercentOfDateRange)*1000);
const newDateRangeInSeconds = dateRangeInSeconds * zoomFactor; const newDateRangeInSeconds = dateRangeInSeconds * zoomFactor;
const newStartDate = anchorTimestampInSeconds.minus(newDateRangeInSeconds*anchorInPercentOfDateRange*1000);
const newEndDate = newStartDate.plus({seconds: newDateRangeInSeconds});;
this.setDateRange(newStartDate, newEndDate);
},
error: (err: any) => {
window.console.error("failed to parse DateValue into DateRange: ", err);
}
})
const newStartDate = anchorTimestampInSeconds.clone().subtract(newDateRangeInSeconds*anchorInPercentOfDateRange, "seconds");
const newEndDate = newStartDate.clone().add({seconds: newDateRangeInSeconds});;
this.setDateRange(newStartDate, newEndDate);
} }
/** /**
@@ -312,27 +332,48 @@ export class PlotViewComponent implements OnInit {
* shiftDate(dateRangeAsString, -0.5, -0.5) will move the range by half its size to older values * shiftDate(dateRangeAsString, -0.5, -0.5) will move the range by half its size to older values
* shiftDate(dateRangeAsString, 1, 1) will move the range by its size to newer values * shiftDate(dateRangeAsString, 1, 1) will move the range by its size to newer values
*/ */
shiftDate(dateRange: string, factorStartDate: number, factorEndDate: number) shiftDate(dateValue: DateValue, factorStartDate: number, factorEndDate: number)
{ {
const dateRangeParsed = this.parseDateRange(dateRange); this.parseDateRange(dateValue).subscribe(
const dateRangeInSeconds = dateRangeParsed.duration.asSeconds(); dateRangeParsed => {
const dateRangeInSeconds = Math.floor(dateRangeParsed.duration.toMillis()/1000);
const newStartDate = dateRangeParsed.startDate.add({seconds: dateRangeInSeconds*factorStartDate});
const newEndDate = dateRangeParsed.endDate.add({seconds: dateRangeInSeconds*factorEndDate}); const newStartDate = dateRangeParsed.startDate.plus({seconds: dateRangeInSeconds*factorStartDate});
const newEndDate = dateRangeParsed.endDate.plus({seconds: dateRangeInSeconds*factorEndDate});
this.setDateRange(newStartDate, newEndDate);
this.setDateRange(newStartDate, newEndDate);
}
);
} }
parseDateRange(dateRangeAsString : string) : DateRange { parseDateRange(dateValue : DateValue) : Observable<DateRange> {
const startDate = moment(dateRangeAsString.slice(0, 19)); return this.service.toDateRange(dateValue);
const endDate = moment(dateRangeAsString.slice(22, 41)); /*
.pipe(map((dateRangeAsString:string) => {
return { const startDate = DateTime.fromFormat(dateRangeAsString.slice(0, 19), this.DATE_PATTERN );
startDate: startDate, const endDate = DateTime.fromFormat(dateRangeAsString.slice(22, 41), this.DATE_PATTERN );
endDate: endDate,
duration: moment.duration(endDate.diff(startDate))
}; return {
startDate: startDate,
endDate: endDate,
duration: endDate.diff(startDate),
};
}));
*/
} }
dataSeries(): Array<DataSeriesStats> {
return this.stats ? this.stats.dataSeriesStats : [];
}
pointTypeClass(typeAndColor: DashTypeAndColor): string {
return "plot-details-plotType"
+" plot-details-plotType_"+typeAndColor.pointType
+" plot-details-plotType_"+typeAndColor.color.toLocaleLowerCase();
}
} }
export class SelectionRange { export class SelectionRange {
@@ -360,7 +401,8 @@ export class LoadingEvent {
} }
export class DateRange { export class DateRange {
startDate: any; constructor(
endDate: any; public startDate: DateTime,
duration: any; public endDate: DateTime,
public duration: Duration){}
} }

View File

@@ -1,195 +1,371 @@
import { Injectable, OnInit } from '@angular/core'; import { Injectable, OnInit } from "@angular/core";
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from "@angular/common/http";
import { Observable } from 'rxjs'; import { Observable } from "rxjs";
import { map } from 'rxjs/operators'; import { map } from "rxjs/operators";
import { DateValue } from "./components/datepicker/date-picker.component";
import { DateRange } from "./plot-view/plot-view.component";
import { DateTime } from "luxon";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root",
}) })
export class PlotService { export class PlotService {
readonly DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss";
plotTypes: Array<PlotType>; plotTypes: Array<PlotType>;
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
this.plotTypes = new Array<PlotType>(); this.plotTypes = new Array<PlotType>();
this.plotTypes.push(new PlotType("SCATTER","Scatter","scatter-chart2",true,DataType.Time,DataType.Duration)); this.plotTypes.push(
this.plotTypes.push(new PlotType("CUM_DISTRIBUTION", "Cumulative Distribution", "cumulative-distribution-chart", true, DataType.Percent, DataType.Duration)); new PlotType(
this.plotTypes.push(new PlotType("HISTOGRAM", "Histogram", "histogram", true, DataType.HistogramBin, DataType.HistogramCount)); "SCATTER",
this.plotTypes.push(new PlotType("PARALLEL", "Parallel Requests", "parallel-requests-chart", true, DataType.Time, DataType.Count)); "Scatter",
this.plotTypes.push(new PlotType("BAR", "Bar (number of requests)", "bar-chart", true, DataType.Group, DataType.Count)); "scatter-chart2",
this.plotTypes.push(new PlotType("BOX", "Box", "box-plot", true, DataType.Time, DataType.Duration)); true,
DataType.Time,
this.plotTypes.push(new PlotType("HEATMAP", "Heatmap", "heatmap", false, DataType.Other, DataType.Other)); DataType.Duration,
this.plotTypes.push(new PlotType("CONTOUR", "Contour", "contour-chart", false, DataType.Time, DataType.Duration)); ),
this.plotTypes.push(new PlotType("RIDGELINES", "Ridgelines", "ridgelines", false, DataType.Other, DataType.Other)); );
this.plotTypes.push(new PlotType("QQ", "Quantile-Quantile", "quantile-quantile", false, DataType.Other, DataType.Other)); this.plotTypes.push(
this.plotTypes.push(new PlotType("VIOLIN", "Violin", "violin-chart", false, DataType.Group, DataType.Duration)); new PlotType(
this.plotTypes.push(new PlotType("STRIP", "Strip", "strip-chart", false, DataType.Group, DataType.Duration)); "CUM_DISTRIBUTION",
this.plotTypes.push(new PlotType("PIE", "Pie", "pie-chart", false, DataType.Other, DataType.Other)); "Cumulative Distribution",
this.plotTypes.push(new PlotType("STEP_FIT", "Step Fit", "step-fit", false, DataType.Other, DataType.Other)); "cumulative-distribution-chart",
this.plotTypes.push(new PlotType("LAG", "Lag", "lag-plot", false, DataType.Other, DataType.Other)); true,
this.plotTypes.push(new PlotType("ACF", "ACF", "acf-plot", false, DataType.Other, DataType.Other)); DataType.Percent,
} DataType.Duration,
),
getPlotTypes(): Array<PlotType> { );
return this.plotTypes.filter(plotType => plotType.active); this.plotTypes.push(
} new PlotType(
"HISTOGRAM",
getTagFields(): Observable<Array<string>> { "Histogram",
return this.http.get<Array<string>>('//'+window.location.hostname+':'+window.location.port+'/api/fields'); "histogram",
} true,
DataType.HistogramBin,
autocomplete(query: string, caretIndex: number, resultMode: ResultMode): Observable<AutocompleteResult> DataType.HistogramCount,
{ ),
const options = { );
params: new HttpParams() this.plotTypes.push(
.set('caretIndex', ""+caretIndex) new PlotType(
.set('query', query) "PARALLEL",
.set('resultMode', resultMode) "Parallel Requests",
}; "parallel-requests-chart",
return this.http.get<AutocompleteResult>('//'+window.location.hostname+':'+window.location.port+'/api/autocomplete', options); true,
DataType.Time,
DataType.Count,
),
);
this.plotTypes.push(
new PlotType(
"BAR",
"Bar (number of requests)",
"bar-chart",
true,
DataType.Group,
DataType.Count,
),
);
this.plotTypes.push(
new PlotType(
"BOX",
"Box",
"box-plot",
true,
DataType.Time,
DataType.Duration,
),
);
this.plotTypes.push(
new PlotType(
"HEATMAP",
"Heatmap",
"heatmap",
false,
DataType.Other,
DataType.Other,
),
);
this.plotTypes.push(
new PlotType(
"CONTOUR",
"Contour",
"contour-chart",
false,
DataType.Time,
DataType.Duration,
),
);
this.plotTypes.push(
new PlotType(
"RIDGELINES",
"Ridgelines",
"ridgelines",
false,
DataType.Other,
DataType.Other,
),
);
this.plotTypes.push(
new PlotType(
"QQ",
"Quantile-Quantile",
"quantile-quantile",
false,
DataType.Other,
DataType.Other,
),
);
this.plotTypes.push(
new PlotType(
"VIOLIN",
"Violin",
"violin-chart",
false,
DataType.Group,
DataType.Duration,
),
);
this.plotTypes.push(
new PlotType(
"STRIP",
"Strip",
"strip-chart",
false,
DataType.Group,
DataType.Duration,
),
);
this.plotTypes.push(
new PlotType(
"PIE",
"Pie",
"pie-chart",
false,
DataType.Other,
DataType.Other,
),
);
this.plotTypes.push(
new PlotType(
"STEP_FIT",
"Step Fit",
"step-fit",
false,
DataType.Other,
DataType.Other,
),
);
this.plotTypes.push(
new PlotType(
"LAG",
"Lag",
"lag-plot",
false,
DataType.Other,
DataType.Other,
),
);
this.plotTypes.push(
new PlotType(
"ACF",
"ACF",
"acf-plot",
false,
DataType.Other,
DataType.Other,
),
);
} }
abort(submitterId: string): Observable<void>{ getPlotTypes(): Array<PlotType> {
return this.http.delete<void>('//'+window.location.hostname+':'+window.location.port+'/api/plots/'+submitterId) return this.plotTypes.filter((plotType) => plotType.active);
} }
sendPlotRequest(plotRequest: PlotRequest): Observable<PlotResponse>{ getTagFields(): Observable<Array<string>> {
return this.http.get<Array<string>>(
"//" + window.location.hostname + ":" + window.location.port +
"/api/fields",
);
}
autocomplete(
query: string,
caretIndex: number,
resultMode: ResultMode,
): Observable<AutocompleteResult> {
const options = {
params: new HttpParams()
.set("caretIndex", "" + caretIndex)
.set("query", query)
.set("resultMode", resultMode),
};
return this.http.get<AutocompleteResult>(
"//" + window.location.hostname + ":" + window.location.port +
"/api/autocomplete",
options,
);
}
abort(submitterId: string): Observable<void> {
return this.http.delete<void>(
"//" + window.location.hostname + ":" + window.location.port +
"/api/plots/" + submitterId,
);
}
sendPlotRequest(plotRequest: PlotRequest): Observable<PlotResponse> {
//console.log("send plot request: "+ JSON.stringify(plotRequest)); //console.log("send plot request: "+ JSON.stringify(plotRequest));
const result = this.http.post<PlotResponse>('//'+window.location.hostname+':'+window.location.port+'/api/plots', plotRequest); const result = this.http.post<PlotResponse>(
"//" + window.location.hostname + ":" + window.location.port +
"/api/plots",
plotRequest,
);
return result.pipe(map(this.enrichStats)); return result.pipe(map(this.enrichStats));
} }
enrichStats(response: PlotResponse): PlotResponse { enrichStats(response: PlotResponse): PlotResponse {
let maxAvgRatio = 0; let maxAvgRatio = 0;
let x : DataSeriesStats[] = response.stats.dataSeriesStats; let x: DataSeriesStats[] = response.stats.dataSeriesStats;
for (const row in x){ for (const row in x) {
for (const col in x){ for (const col in x) {
maxAvgRatio = Math.max(maxAvgRatio, x[row].average / x[col].average); maxAvgRatio = Math.max(maxAvgRatio, x[row].average / x[col].average);
} }
} }
response.stats.maxAvgRatio = maxAvgRatio; response.stats.maxAvgRatio = maxAvgRatio;
return response; return response;
} }
getFilterDefaults(): Observable<FilterDefaults>{ getFilterDefaults(): Observable<FilterDefaults> {
return this.http.get<FilterDefaults>('//'+window.location.hostname+':'+window.location.port+'/api/filters/defaults') return this.http.get<FilterDefaults>(
} "//" + window.location.hostname + ":" + window.location.port +
"/api/filters/defaults",
splitQuery(query: string, splitBy:string) : Observable<Array<string>>{
const q = "("+query+") and "+splitBy+"=";
return this.autocomplete(q, q.length+1, ResultMode.FULL_VALUES).pipe(
map(
(autocompleteResult: AutocompleteResult) => autocompleteResult.proposals.map((suggestion:Suggestion) => suggestion.value)
)
); );
} }
splitQuery(query: string, splitBy: string): Observable<Array<string>> {
const q = "(" + query + ") and " + splitBy + "=";
return this.autocomplete(q, q.length + 1, ResultMode.FULL_VALUES).pipe(
map(
(autocompleteResult: AutocompleteResult) =>
autocompleteResult.proposals.map((suggestion: Suggestion) =>
suggestion.value
),
),
);
}
toDateRange(dateValue: DateValue): Observable<DateRange> {
return this.http.post<{start: string, end:string, startEpochMilli: number, endEpochMilli: number}>("//" + window.location.hostname+":" + window.location.port +"/api/dates",dateValue)
.pipe(map((data) => {
const startDate = DateTime.fromFormat(data.start.slice(0, -1), this.DATE_PATTERN );
const endDate = DateTime.fromFormat(data.end.slice(0, -1), this.DATE_PATTERN );
return {
startDate: startDate,
endDate: endDate,
duration: endDate.diff(startDate),
};
}));
}
} }
export class PlotType { export class PlotType {
id: string; constructor(
name: string; public id: string,
icon: string public name: string,
active: boolean; public icon: string,
xAxis: DataType; public active: boolean,
yAxis: DataType; public xAxis: DataType,
public yAxis: DataType,
constructor(id: string, name: string, icon: string, active: boolean, xAxis: DataType, yAxis: DataType) { ) {
this.id = id;
this.name = name;
this.icon = icon;
this.active = active;
this.xAxis = xAxis;
this.yAxis = yAxis;
} }
compatible(others: Array<PlotType>) : boolean { compatible(others: Array<PlotType>): boolean {
var xAxisTypes = new Set([this.xAxis]); var xAxisTypes = new Set([this.xAxis]);
var yAxisTypes = new Set([this.yAxis]); var yAxisTypes = new Set([this.yAxis]);
for(var i = 0; i < others.length; i++){ for (var i = 0; i < others.length; i++) {
var other = others[i]; var other = others[i];
xAxisTypes.add(other.xAxis); xAxisTypes.add(other.xAxis);
yAxisTypes.add(other.yAxis); yAxisTypes.add(other.yAxis);
} }
return xAxisTypes.size <= 2 && yAxisTypes.size <= 2; return xAxisTypes.size <= 2 && yAxisTypes.size <= 2;
} }
} }
export class TagField { export class TagField {
name: string; name: string;
constructor(name: string) { constructor(name: string) {
this.name = name; this.name = name;
} }
} }
export enum DataType { export enum DataType {
Time, Time,
Duration, Duration,
Percent, Percent,
Count, Count,
Group, Group,
Metric, Metric,
HistogramBin, HistogramBin,
HistogramCount, HistogramCount,
Other Other,
} }
export class AxesTypes { export class AxesTypes {
x : Array<DataType>; x: Array<DataType>;
y : Array<DataType>; y: Array<DataType>;
constructor(x: Array<DataType>, y : Array<DataType>) { constructor(x: Array<DataType>, y: Array<DataType>) {
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
hasXAxis(type : DataType){ hasXAxis(type: DataType) {
return this.x.includes(type); return this.x.includes(type);
} }
hasYAxis(type : DataType){ hasYAxis(type: DataType) {
return this.y.includes(type); return this.y.includes(type);
} }
/** /**
* return the 1-indexed axis data type, e.g. getXAxisDataType(1) for the x1 axis * return the 1-indexed axis data type, e.g. getXAxisDataType(1) for the x1 axis
*/ */
getXAxisDataType(index: number){ getXAxisDataType(index: number) {
if (this.x.length+1 >= index){ if (this.x.length + 1 >= index) {
return this.x[index-1]; return this.x[index - 1];
} }
return undefined; return undefined;
} }
/** /**
* return the 1-indexed axis data type, e.g. getYAxisDataType(1) for the x1 axis * return the 1-indexed axis data type, e.g. getYAxisDataType(1) for the x1 axis
*/ */
getYAxisDataType(index: number){ getYAxisDataType(index: number) {
if (this.y.length+1 >= index){ if (this.y.length + 1 >= index) {
return this.y[index-1]; return this.y[index - 1];
} }
return undefined; return undefined;
} }
toString() { toString() {
const x1 = this.getXAxisDataType(1); const x1 = this.getXAxisDataType(1);
const y1 = this.getYAxisDataType(1); const y1 = this.getYAxisDataType(1);
const x2 = this.getXAxisDataType(2); const x2 = this.getXAxisDataType(2);
const y2 = this.getYAxisDataType(2); const y2 = this.getYAxisDataType(2);
return (x1 ? "x1:"+DataType[x1] : "") return (x1 ? "x1:" + DataType[x1] : "") +
+ (y1 ? " y1:"+DataType[y1] : "") (y1 ? " y1:" + DataType[y1] : "") +
+ (x2 ? " x2:"+DataType[x2] : "") (x2 ? " x2:" + DataType[x2] : "") +
+ (y2 ? " y2:"+DataType[y2] : ""); (y2 ? " y2:" + DataType[y2] : "");
} }
} }
@@ -197,12 +373,12 @@ export class Suggestion {
constructor( constructor(
public value: string, public value: string,
public newQuery: string, public newQuery: string,
public newCaretPosition: number){} public newCaretPosition: number,
) {}
} }
export class AutocompleteResult {
export class AutocompleteResult{ constructor(public proposals: Array<Suggestion>) {}
constructor(public proposals: Array<Suggestion>){}
} }
export type RenderOptionsMap = { export type RenderOptionsMap = {
@@ -217,86 +393,94 @@ export class PlotRequest {
constructor( constructor(
public submitterId: string, public submitterId: string,
public config: PlotConfig, public config: PlotConfig,
public renders: RenderOptionsMap public renders: RenderOptionsMap,
){} ) {}
copy(): PlotRequest { copy(): PlotRequest {
return JSON.parse(JSON.stringify(this)); return JSON.parse(JSON.stringify(this));
} }
} }
export class PlotConfig { export class PlotConfig {
constructor( public query : string, constructor(
public groupBy : Array<string>, public query: string,
public limitBy : string, public groupBy: Array<string>,
public limit : number, public limitBy: string,
public y1:YAxisDefinition, public limit: number,
public y2:YAxisDefinition|undefined, public y1: YAxisDefinition,
public dateRange : string, public y2: YAxisDefinition | undefined,
public aggregates : Array<string>, public dateRange: DateValue,
public aggregates: Array<string>,
public intervalUnit: string, public intervalUnit: string,
public intervalValue: number, public intervalValue: number,
public renderBarChartTickLabels: boolean = false,) {} public renderBarChartTickLabels: boolean = false,
) {}
} }
export class RenderOptions { export class RenderOptions {
constructor( constructor(
public height: number, public height: number,
public width: number, public width: number,
public keyOutside: boolean, public showKey: boolean,
public renderLabels: boolean) {} public renderLabels: boolean,
) {}
} }
export class YAxisDefinition { export class YAxisDefinition {
constructor( constructor(
public axisScale : string, public axisScale: string,
public rangeMin : number, public rangeMin: number,
public rangeMax : number, public rangeMax: number,
public rangeUnit : string){} public rangeUnit: string,
) {}
} }
export class PlotResponse { export class PlotResponse {
constructor( constructor(
public stats : PlotResponseStats, public stats: PlotResponseStats,
public rendered: RenderedImages){} public rendered: RenderedImages,
) {}
} }
export class PlotResponseStats { export class PlotResponseStats {
constructor( constructor(
public maxValue : number, public maxValue: number,
public values : number, public values: number,
public average : number, public average: number,
public plottedValues : number, public plottedValues: number,
public maxAvgRatio: number, public maxAvgRatio: number,
public dataSeriesStats : Array<DataSeriesStats>){} public dataSeriesStats: Array<DataSeriesStats>,
) {}
} }
export class DataSeriesStats { export class DataSeriesStats {
constructor( constructor(
public name: string, public name: string,
public values : number, public values: number,
public maxValue : number, public maxValue: number,
public average : number , public average: number,
public plottedValues : number, public plottedValues: number,
public dashTypeAndColor: DashTypeAndColor, public dashTypeAndColor: DashTypeAndColor,
public percentiles: Map<string, number>){} public percentiles: Map<string, number>,
) {}
} }
export class DashTypeAndColor { export class DashTypeAndColor {
constructor( constructor(
public color: string, public color: string,
public pointType: number) {} public pointType: number,
) {}
} }
export class FilterDefaults { export class FilterDefaults {
constructor( constructor(
public groupBy: Array<string>, public groupBy: Array<string>,
public fields: Array<string>, public fields: Array<string>,
public splitBy: string){} public splitBy: string,
) {}
} }
export enum ResultMode { export enum ResultMode {
CUT_AT_DOT = "CUT_AT_DOT", CUT_AT_DOT = "CUT_AT_DOT",
FULL_VALUES = "FULL_VALUES" FULL_VALUES = "FULL_VALUES",
} }

View File

@@ -8,8 +8,8 @@ describe('QueryAutocompleteComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ QueryAutocompleteComponent ] imports: [QueryAutocompleteComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,52 +1,64 @@
import { Component, OnInit, Input, ViewChild } from '@angular/core'; import { Component, OnInit, Input, ViewChild, AfterViewInit } from '@angular/core';
import {UntypedFormControl} from '@angular/forms'; import { FormControl, UntypedFormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import {startWith, map} from 'rxjs/operators'; import {startWith, map} from 'rxjs/operators';
import {MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete';
import { PlotService, PlotType, AutocompleteResult, Suggestion, ResultMode } from '../plot.service'; import { PlotService, PlotType, AutocompleteResult, Suggestion, ResultMode } from '../plot.service';
import { MatInput } from '@angular/material/input';
import { NgFor, AsyncPipe } from '@angular/common';
import { MatOption } from '@angular/material/core';
@Component({ @Component({
selector: 'pdb-query-autocomplete', selector: 'pdb-query-autocomplete',
templateUrl: './query-autocomplete.component.html', templateUrl: './query-autocomplete.component.html',
styleUrls: ['./query-autocomplete.component.scss'] styleUrls: ['./query-autocomplete.component.scss'],
standalone: true,
imports: [MatInput, FormsModule, MatAutocompleteTrigger, ReactiveFormsModule, MatAutocomplete, NgFor, MatOption, AsyncPipe]
}) })
export class QueryAutocompleteComponent implements OnInit { export class QueryAutocompleteComponent implements OnInit {
queryField = new UntypedFormControl(''); queryField = new FormControl<Suggestion>(new Suggestion("","",0));
suggestions = new UntypedFormControl(); suggestions = new UntypedFormControl();
filteredSuggestions!: Observable<Suggestion[]>; filteredSuggestions!: Observable<Suggestion[]>;
query : string = ""; query : string = "";
suggestionFetcherEnabled = true;
@ViewChild(MatAutocompleteTrigger) @ViewChild(MatAutocompleteTrigger)
autocomplete!: MatAutocompleteTrigger; autocomplete!: MatAutocompleteTrigger;
constructor(private plotService: PlotService) {} constructor(private plotService: PlotService) {
}
ngOnInit() { ngOnInit() {
const that = this;
this.query = ""; this.query = "";
this.queryField.valueChanges.subscribe(function(value){ this.queryField.valueChanges.subscribe((value) =>{
if (typeof value == "string") { if (value != null) {
that.query = value; if (typeof value == "string") {
}else{ this.query = value;
that.query = value.newQuery; }else{
this.query = value.newQuery;
const el : HTMLInputElement = <HTMLInputElement>document.getElementById('query-autocomplete-input');
el.selectionStart=value.newCaretPosition;
el.selectionEnd=value.newCaretPosition;
}
var el : HTMLInputElement = <HTMLInputElement>document.getElementById('query-autocomplete-input');
el.selectionStart=value.newCaretPosition;
el.selectionEnd=value.newCaretPosition;
that.fetchSuggestions(value.newCaretPosition); if (this.suggestionFetcherEnabled) {
this.fetchSuggestions(value.newCaretPosition);
}
} }
}); });
this.filteredSuggestions = this.suggestions.valueChanges.pipe( this.filteredSuggestions = this.suggestions.valueChanges.pipe(
map(value => value) map(value => value)
); );
} }
onKey(event: any) { onKey(event: any) {
//console.log(event); //console.log(event);
if (event.key == "ArrowDown" || event.key == "ArrowUp"){ if (event.key == "ArrowDown" || event.key == "ArrowUp"){
@@ -61,23 +73,21 @@ export class QueryAutocompleteComponent implements OnInit {
const that = this; const that = this;
const query = typeof this.queryField.value == "string" const query = typeof this.queryField.value == "string"
? this.queryField.value ? this.queryField.value
: this.queryField.value.newQuery; : this.queryField.value!.newQuery;
this.plotService this.plotService
.autocomplete(query, caretIndex, ResultMode.CUT_AT_DOT) .autocomplete(query, caretIndex, ResultMode.CUT_AT_DOT)
.subscribe( .subscribe({
(data: AutocompleteResult) => {// success path next: (data: AutocompleteResult) => {
//console.log(JSON.stringify(data.proposals));
that.suggestions.setValue(data.proposals); that.suggestions.setValue(data.proposals);
that.autocomplete.openPanel(); that.autocomplete.openPanel();
}, },
(error:any) => console.log(error) error: (error:any) => console.log(error)
}
); );
} }
displaySuggestion(suggestion?: Suggestion): string { displaySuggestion(suggestion?: Suggestion): string {
//console.log("suggestion: "+JSON.stringify(suggestion));
return suggestion ? suggestion.newQuery : ''; return suggestion ? suggestion.newQuery : '';
} }
} }

View File

@@ -8,8 +8,8 @@ describe('UploadPageComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ UploadPageComponent ] imports: [UploadPageComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'pdb-upload-page', selector: 'pdb-upload-page',
templateUrl: './upload-page.component.html', templateUrl: './upload-page.component.html',
styleUrls: ['./upload-page.component.scss'] styleUrls: ['./upload-page.component.scss'],
standalone: true
}) })
export class UploadPageComponent implements OnInit { export class UploadPageComponent implements OnInit {

View File

@@ -1,108 +1,155 @@
<div id="visualization"> <div id="visualization">
<div id="query-box"> <div id="query-box">
<pdb-query-autocomplete #query></pdb-query-autocomplete> <pdb-query-autocomplete #query></pdb-query-autocomplete>
</div> </div>
<div id="date-box"> <div id="date-box">
<app-date-picker #datePicker></app-date-picker>
<!--
<mat-form-field class="pdb-form-full-width"> <mat-form-field class="pdb-form-full-width">
<mat-label>Date Range:</mat-label> <mat-label>Date Range:</mat-label>
<input matInput id="search-date-range" value="dateRange" name="dates" /> <input matInput id="search-date-range" value="dateRange" name="dates" />
</mat-form-field> </mat-form-field>
-->
</div> </div>
<div id="filters"> <div id="filters">
<div id="filterpanel"> <div id="filterpanel">
<mat-form-field class="pdb-form-full-width"> <mat-form-field class="pdb-form-full-width">
<mat-label>Type:</mat-label> <mat-label>Type:</mat-label>
<mat-select multiple [(ngModel)]="selectedPlotType" (ngModelChange)="changePlotType($event)"> <mat-select
<mat-option *ngFor="let plotType of plotTypes" [value]="plotType" [disabled]="!plotType.active"> multiple
<img src="assets/img/{{plotType.icon}}.svg" class="icon-select" /> {{plotType.name}} [(ngModel)]="selectedPlotType"
(ngModelChange)="changePlotType($event)"
>
<mat-option
*ngFor="let plotType of plotTypes"
[value]="plotType"
[disabled]="!plotType.active"
>
<img src="assets/img/{{ plotType.icon }}.svg" class="icon-select" />
{{ plotType.name }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="pdb-form-full-width"> <mat-form-field class="pdb-form-full-width">
<mat-label>Group By:</mat-label> <mat-label>Group By:</mat-label>
<mat-select multiple [(value)]="groupBy"> <mat-select multiple [(value)]="groupBy">
<mat-option *ngFor="let tagField of tagFields" [value]="tagField">{{tagField.name}}</mat-option> <mat-option *ngFor="let tagField of tagFields" [value]="tagField">{{
tagField.name
}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<pdb-limit-by #limitbycomponent></pdb-limit-by> <pdb-limit-by #limitbycomponent></pdb-limit-by>
<div [hidden]="!selectedPlotTypesContains(['BAR', 'BOX'])"> <div [hidden]="!selectedPlotTypesContains(['BAR', 'BOX'])">
<mat-form-field > <mat-form-field>
<mat-label>Intervals (only bar chart):</mat-label> <mat-label>Intervals (only bar chart):</mat-label>
<mat-select [(value)]="intervalUnit"> <mat-select [(value)]="intervalUnit">
<mat-option value="NO_INTERVAL">-</mat-option> <mat-option value="NO_INTERVAL">-</mat-option>
<mat-option value="SECOND">second</mat-option> <mat-option value="SECOND">second</mat-option>
<mat-option value="MINUTE">minute</mat-option> <mat-option value="MINUTE">minute</mat-option>
<mat-option value="HOUR">hour</mat-option> <mat-option value="HOUR">hour</mat-option>
<mat-option value="DAY">day</mat-option> <mat-option value="DAY">day</mat-option>
<mat-option value="WEEK">week</mat-option> <mat-option value="WEEK">week</mat-option>
<mat-option value="MONTH">month</mat-option> <mat-option value="MONTH">month</mat-option>
<mat-option value="YEAR">year</mat-option> <mat-option value="YEAR">year</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div [hidden]="!selectedPlotTypesContains(['BAR', 'BOX'])"> <div [hidden]="!selectedPlotTypesContains(['BAR', 'BOX'])">
<mat-checkbox [(ngModel)]="renderBarChartTickLabels">Show Tic Labels (bar chart)</mat-checkbox> <mat-checkbox [(ngModel)]="renderBarChartTickLabels"
>Show Tic Labels (bar chart)</mat-checkbox
>
</div> </div>
<pdb-y-axis-definition #y1AxisDefinitionComponent yIndex="1"></pdb-y-axis-definition> <pdb-y-axis-definition
<pdb-y-axis-definition #y2AxisDefinitionComponent yIndex="2" [hidden]="!y2AxisAvailable"></pdb-y-axis-definition> #y1AxisDefinitionComponent
yIndex="1"
<mat-checkbox [(ngModel)]="enableGallery">Gallery</mat-checkbox> ></pdb-y-axis-definition>
<pdb-y-axis-definition
<mat-form-field *ngIf="enableGallery" class="pdb-form-full-width"> #y2AxisDefinitionComponent
yIndex="2"
[hidden]="!y2AxisAvailable"
></pdb-y-axis-definition>
<mat-checkbox
*ngIf="galleryEnabled"
[(ngModel)]="enableGallery"
(click)="toggleGallery($event)"
>Gallery</mat-checkbox
>
<mat-form-field *ngIf="enableGallery" class="pdb-form-full-width">
<mat-label>Split By:</mat-label> <mat-label>Split By:</mat-label>
<mat-select [(value)]="splitBy"> <mat-select [(value)]="splitBy">
<mat-option *ngFor="let tagField of tagFields" [value]="tagField">{{tagField.name}}</mat-option> <mat-option *ngFor="let tagField of tagFields" [value]="tagField">{{
tagField.name
}}</mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="splitBy == null || true"> <mat-error *ngIf="splitBy == null || true">
Please select a value! Please select a value!
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div id="plot-button-bar"> <div id="plot-button-bar">
<button <a
*ngIf="!enableGallery && !plotJobActive" mat-icon-button
[disabled]="plotJobActive" [routerLink]="['/vis']"
mat-button [queryParams]="{ config: serializedConfig() }"
matTooltip="Create Plot" target="_blank"
(click)="plot()"> aria-label="open new window with the same search"
<img src="assets/img/scatter-chart2.svg" class="icon-inline" aria-hidden="true" title="create plot" /> title="open new window with the same search"
><img src="assets/img/link.svg" aria-hidden="true"
/></a>
<button
*ngIf="!enableGallery && !plotJobActive"
[disabled]="plotJobActive"
mat-button
matTooltip="Create Plot"
(click)="plot()"
>
<img
src="assets/img/scatter-chart2.svg"
class="icon-inline"
aria-hidden="true"
title="create plot"
/>
Plot Plot
</button> </button>
<button <button
*ngIf="enableGallery && !plotJobActive" *ngIf="enableGallery && !plotJobActive"
mat-button mat-button
matTooltip="Create Gallery" matTooltip="Create Gallery"
(click)="gallery()" (click)="gallery()"
[disabled]="this.splitBy == null"> [disabled]="this.splitBy == null"
<img src="assets/img/four-squares-line.svg" class="icon-inline" aria-hidden="true" title="Create Gallery (only active if 'Split' is set)" /> >
<img
src="assets/img/four-squares-line.svg"
class="icon-inline"
aria-hidden="true"
title="Create Gallery (only active if 'Split' is set)"
/>
Gallery Gallery
</button> </button>
<button <button
*ngIf="plotJobActive" *ngIf="plotJobActive"
mat-button mat-button
(click)="abort()" (click)="abort()"
matTooltip="abort"><img src="assets/img/close.svg" class="icon-inline" /> Abort</button> matTooltip="abort"
>
<img src="assets/img/close.svg" class="icon-inline" /> Abort
</button>
</div> </div>
</div> </div>
</div> </div>
<div id="results"> <div id="results">
<pdb-plot-view <pdb-plot-view
#plotView #plotView
(loadingEvent)="loading($event)" (loadingEvent)="loading($event)"
(dateRangeUpdateEvent)="updateDateRange($event)"></pdb-plot-view> (dateRangeUpdateEvent)="updateDateRange($event)"
<pdb-gallery-view ></pdb-plot-view>
#galleryView> <pdb-gallery-view #galleryView> </pdb-gallery-view>
</pdb-gallery-view>
</div> </div>
</div> </div>

View File

@@ -15,11 +15,11 @@
grid: grid:
"query-box query-box date-box" auto "query-box query-box date-box" auto
"filters results results" 1fr "filters results results" 1fr
/ 25.5em 3fr 24em; / 25.5em 3fr auto;
} }
} }
@media screen and (max-width: 1000px) { @media screen and (max-width: 900px) {
#visualization { #visualization {
display: grid; display: grid;
margin: 0; margin: 0;
@@ -27,7 +27,7 @@
grid: grid:
"query-box" auto "query-box" auto
"date-box" auto "date-box" auto
"filters" auto "filters" min-content
"results" 1fr "results" 1fr
/ 1fr; / 1fr;
} }
@@ -43,6 +43,8 @@
#date-box{ #date-box{
grid-area: date-box; grid-area: date-box;
display: flex;
align-items: center;
} }
@@ -67,5 +69,7 @@
} }
#plot-button-bar { #plot-button-bar {
text-align: right; display: flex;
justify-content: space-between;
} }

View File

@@ -8,8 +8,8 @@ describe('VisualizationPageComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ VisualizationPageComponent ] imports: [VisualizationPageComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,138 +1,254 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import {
import { PlotService, PlotType, PlotRequest, PlotResponse, TagField, FilterDefaults, DataType, YAxisDefinition, AxesTypes, PlotConfig, RenderOptions, RenderOptionsMap } from '../plot.service'; AfterViewInit,
import { UntypedFormControl, Validators } from '@angular/forms'; Component,
import { MatSnackBar } from '@angular/material/snack-bar'; Input,
import { LimitByComponent } from '../limit-by/limit-by.component'; OnInit,
import { YAxisDefinitionComponent } from '../y-axis-definition/y-axis-definition.component'; ViewChild,
import { QueryAutocompleteComponent } from '../query-autocomplete/query-autocomplete.component'; } from "@angular/core";
import { PlotViewComponent, SelectionRange, DateAnchor, LoadingEvent } from '../plot-view/plot-view.component'; import {
import { GalleryViewComponent } from '../gallery-view/gallery-view.component'; AxesTypes,
import * as moment from 'moment'; DataType,
import { WidgetDimensions } from '../dashboard.service'; FilterDefaults,
PlotConfig,
PlotRequest,
PlotService,
PlotType,
RenderOptions,
RenderOptionsMap,
Suggestion,
TagField,
} from "../plot.service";
import { UntypedFormControl, FormsModule } from "@angular/forms";
import { MatSnackBar } from "@angular/material/snack-bar";
import { LimitByComponent } from "../limit-by/limit-by.component";
import { YAxisDefinitionComponent } from "../y-axis-definition/y-axis-definition.component";
import { QueryAutocompleteComponent } from "../query-autocomplete/query-autocomplete.component";
import {
DateRange,
LoadingEvent,
PlotViewComponent,
} from "../plot-view/plot-view.component";
import { GalleryViewComponent } from "../gallery-view/gallery-view.component";
import { WidgetDimensions } from "../dashboard.service";
import {
DatePickerComponent,
DateValue,
} from "../components/datepicker/date-picker.component";
import { MatFormField, MatLabel, MatError } from "@angular/material/form-field";
import { MatSelect } from "@angular/material/select";
import { NgFor, NgIf } from "@angular/common";
import { MatOption } from "@angular/material/core";
import { MatCheckbox } from "@angular/material/checkbox";
import { MatIconAnchor, MatButton } from "@angular/material/button";
import { RouterLink } from "@angular/router";
import { MatTooltip } from "@angular/material/tooltip";
@Component({ @Component({
selector: 'pdb-visualization-page', selector: "pdb-visualization-page",
templateUrl: './visualization-page.component.html', templateUrl: "./visualization-page.component.html",
styleUrls: ['./visualization-page.component.scss'] styleUrls: ["./visualization-page.component.scss"],
standalone: true,
imports: [
QueryAutocompleteComponent,
DatePickerComponent,
MatFormField,
MatLabel,
MatSelect,
FormsModule,
NgFor,
MatOption,
LimitByComponent,
MatCheckbox,
YAxisDefinitionComponent,
NgIf,
MatError,
MatIconAnchor,
RouterLink,
MatButton,
MatTooltip,
PlotViewComponent,
GalleryViewComponent,
],
}) })
export class VisualizationPageComponent implements OnInit { export class VisualizationPageComponent implements OnInit, AfterViewInit {
readonly DATE_PATTERN = "YYYY-MM-DD HH:mm:ss"; // for moment-JS readonly DATE_PATTERN = "YYYY-MM-DD HH:mm:ss"; // for moment-JS
dateRange = new UntypedFormControl('2019-10-05 00:00:00 - 2019-10-11 23:59:59'); @Input()
defaultConfig?: PlotConfig;
@Input()
galleryEnabled = true;
dateRange = new UntypedFormControl(
"2019-10-05 00:00:00 - 2019-10-11 23:59:59",
);
selectedPlotType = new Array<PlotType>(); selectedPlotType = new Array<PlotType>();
plotTypes: Array<any> = []; plotTypes: PlotType[] = [];
tagFields: Array<TagField> = new Array<TagField>(); tagFields: Array<TagField> = new Array<TagField>();
groupBy = new Array<TagField>(); groupBy = new Array<TagField>();
@ViewChild('limitbycomponent') @ViewChild("limitbycomponent")
private limitbycomponent! : LimitByComponent; private limitbycomponent!: LimitByComponent;
@ViewChild("y1AxisDefinitionComponent", { read: YAxisDefinitionComponent })
@ViewChild('y1AxisDefinitionComponent', { read: YAxisDefinitionComponent }) private y1AxisDefinitionComponent!: YAxisDefinitionComponent;
private y1AxisDefinitionComponent! : YAxisDefinitionComponent;
@ViewChild("y2AxisDefinitionComponent", { read: YAxisDefinitionComponent })
@ViewChild('y2AxisDefinitionComponent', { read: YAxisDefinitionComponent }) private y2AxisDefinitionComponent!: YAxisDefinitionComponent;
private y2AxisDefinitionComponent! : YAxisDefinitionComponent;
@ViewChild("query")
@ViewChild('query')
query!: QueryAutocompleteComponent; query!: QueryAutocompleteComponent;
@ViewChild('plotView') @ViewChild("plotView")
plotView!: PlotViewComponent; plotView!: PlotViewComponent;
@ViewChild('galleryView') @ViewChild("galleryView")
galleryView!: GalleryViewComponent; galleryView!: GalleryViewComponent;
@ViewChild("datePicker")
datePicker!: DatePickerComponent;
enableGallery = false; enableGallery = false;
splitBy : TagField | undefined = undefined; splitBy: TagField | undefined = undefined;
y2AxisAvailable = false; y2AxisAvailable = false;
intervalUnit = 'NO_INTERVAL'; intervalUnit = "NO_INTERVAL";
intervalValue = 1; intervalValue = 1;
renderBarChartTickLabels = false; renderBarChartTickLabels = false;
plotJobActive = false; plotJobActive = false;
constructor(private plotService: PlotService, private snackBar: MatSnackBar) { constructor(private plotService: PlotService, private snackBar: MatSnackBar) {
const params = new URLSearchParams(window.location.search);
if (!this.defaultConfig && params.get("config")) {
const config = JSON.parse(params.get("config")!);
this.defaultConfig = config;
}
} }
showError(message:string) { showError(message: string) {
this.snackBar.open(message, "", { this.snackBar.open(message, "", {
duration: 5000, duration: 5000,
verticalPosition: 'top' verticalPosition: "top",
}); });
} }
ngOnInit() { ngOnInit() {
const that = this;
(<any>window).initDatePicker();
this.plotTypes = this.plotService.getPlotTypes(); this.plotTypes = this.plotService.getPlotTypes();
this.selectedPlotType.push(this.plotTypes[0]); this.selectedPlotType.push(this.plotTypes[0]);
that.plotService.getFilterDefaults().subscribe(function(filterDefaults: FilterDefaults) { this.plotService.getFilterDefaults().subscribe(
(filterDefaults: FilterDefaults) => {
filterDefaults.fields.forEach(function(name:string) { filterDefaults.fields.forEach((name: string) => {
that.tagFields.push(new TagField(name)); this.tagFields.push(new TagField(name));
}, (error: any) => {
this.showError(error.error.message);
});
const groupByDefaults = this.defaultConfig
? this.defaultConfig.groupBy
: filterDefaults.groupBy;
this.groupBy = this.tagFields.filter((val) =>
groupByDefaults.includes(val.name)
);
this.splitBy = this.tagFields.find((val) =>
filterDefaults.splitBy == val.name
);
if (this.defaultConfig) {
this.plot();
}
}, },
(error: any) => { );
that.showError(error.error.message);
});
that.groupBy = that.tagFields.filter(val => filterDefaults.groupBy.includes(val.name));
that.splitBy = that.tagFields.find(val => filterDefaults.splitBy == val.name);
});
} }
ngAfterViewInit(): void {
if (this.defaultConfig) {
const c = this.defaultConfig;
this.query.suggestionFetcherEnabled = false;
this.query.queryField.setValue(
new Suggestion(c.query, c.query, c.query.length),
);
this.query.suggestionFetcherEnabled = true;
this.selectedPlotType = this.plotTypes.filter((pt) =>
c.aggregates.includes(pt.id)
);
this.changePlotType(this.selectedPlotType);
this.updateDateRange(c.dateRange, false);
this.limitbycomponent.limitBy = c.limitBy;
this.limitbycomponent.limit = c.limit;
this.intervalUnit = c.intervalUnit;
this.intervalValue = c.intervalValue;
this.y1AxisDefinitionComponent.yAxisScale = c.y1.axisScale;
this.y1AxisDefinitionComponent.minYValue = c.y1.rangeMin;
this.y1AxisDefinitionComponent.maxYValue = c.y1.rangeMax;
this.y1AxisDefinitionComponent.yAxisUnit = c.y1.rangeUnit;
if (c.y2) {
this.y2AxisDefinitionComponent.yAxisScale = c.y2.axisScale;
this.y2AxisDefinitionComponent.minYValue = c.y2.rangeMin;
this.y2AxisDefinitionComponent.maxYValue = c.y2.rangeMax;
this.y2AxisDefinitionComponent.yAxisUnit = c.y2.rangeUnit;
}
}
}
toggleGallery(event: Event) {
this.galleryView.show = this.enableGallery;
}
loading(event: LoadingEvent) { loading(event: LoadingEvent) {
this.plotJobActive = event.loading; this.plotJobActive = event.loading;
} }
updateDateRange(newDateRange: string) { updateDateRange(newDateRange: DateValue, updatePlot = true) {
(<HTMLInputElement>document.getElementById("search-date-range")).value = newDateRange; this.datePicker.setDateValue(newDateRange);
this.plot(); if (updatePlot) {
this.plot();
}
} }
changePlotType(selectedPlotTypes: Array<PlotType>) { changePlotType(selectedPlotTypes: Array<PlotType>) {
const compatiblePlotTypes = this.plotTypes.filter(pt => pt.compatible(selectedPlotTypes)); const compatiblePlotTypes = this.plotTypes.filter((pt) =>
this.plotTypes.forEach(pt => pt.active=false); pt.compatible(selectedPlotTypes)
compatiblePlotTypes.forEach(pt => pt.active=true); );
this.plotTypes.forEach((pt) => pt.active = false);
compatiblePlotTypes.forEach((pt) => pt.active = true);
const axesTypes = this.getAxes(); const axesTypes = this.getAxes();
this.y2AxisAvailable = axesTypes.y.length == 2; this.y2AxisAvailable = axesTypes.y.length == 2;
} }
selectedPlotTypesContains(plotTypeIds: Array<string>){ selectedPlotTypesContains(plotTypeIds: Array<string>) {
return this.selectedPlotType.filter(pt => plotTypeIds.includes(pt.id)).length > 0; return this.selectedPlotType.filter((pt) => plotTypeIds.includes(pt.id))
.length > 0;
} }
dateRangeAsString(): DateValue {
dateRangeAsString() : string { return this.datePicker.getDateValue();
return (<HTMLInputElement>document.getElementById("search-date-range")).value;
} }
gallery(){ gallery() {
if (this.splitBy != null){ if (this.splitBy != null) {
this.plotView.imageUrl = ''; this.plotView.imageUrl = "";
this.plotView.stats = null; this.plotView.stats = null;
this.galleryView.show=true; this.galleryView.show = true;
const request = this.createPlotRequest(); const request = this.createPlotRequest();
this.galleryView.renderGallery(request, this.splitBy.name); this.galleryView.renderGallery(request, this.splitBy.name);
} else { } else {
console.error("variable splitBy was null when rendering gallery"); console.error("variable splitBy was null when rendering gallery");
} }
} }
getAxes() : AxesTypes { getAxes(): AxesTypes {
const x = new Array<DataType>(); const x = new Array<DataType>();
const y = new Array<DataType>(); const y = new Array<DataType>();
for(var i = 0; i < this.selectedPlotType.length; i++){ for (var i = 0; i < this.selectedPlotType.length; i++) {
var plotType = this.selectedPlotType[i]; var plotType = this.selectedPlotType[i];
if (!x.includes(plotType.xAxis)) { if (!x.includes(plotType.xAxis)) {
x.push(plotType.xAxis); x.push(plotType.xAxis);
@@ -141,67 +257,84 @@ export class VisualizationPageComponent implements OnInit {
y.push(plotType.yAxis); y.push(plotType.yAxis);
} }
} }
return new AxesTypes(x,y); return new AxesTypes(x, y);
} }
abort() { abort() {
this.plotService.abort((<any>window).submitterId).subscribe({ this.plotService.abort((<any> window).submitterId).subscribe({
complete: () => { complete: () => {
} },
}); });
} }
plot(){ plot() {
const config = this.createPlotConfig(); const config = this.createPlotConfig();
this.plotView.plot(config, this.plotDimensionSupplier); this.plotView.plot(config, this.plotDimensionSupplier);
} }
plotDimensionSupplier(): WidgetDimensions{ plotDimensionSupplier(): WidgetDimensions {
const results = document.getElementById("results"); const results = document.getElementById("results");
return new WidgetDimensions( return new WidgetDimensions(
results != null ? results.offsetWidth-1 : 1024, results != null ? results.offsetWidth - 1 : 1024,
results != null ? results.offsetHeight-1: 1024); results != null ? results.offsetHeight - 1 : 1024,
);
} }
createPlotConfig(): PlotConfig { createPlotConfig(): PlotConfig {
const aggregates = new Array<string>(); const aggregates = new Array<string>();
this.selectedPlotType.forEach(a => aggregates.push(a.id)); this.selectedPlotType.forEach((a) => aggregates.push(a.id));
const y1 = this.y1AxisDefinitionComponent.getAxisDefinition(); const y1 = this.y1AxisDefinitionComponent.getAxisDefinition();
const y2 = this.y2AxisDefinitionComponent ? this.y2AxisDefinitionComponent.getAxisDefinition() : undefined; const y2 = this.y2AxisDefinitionComponent
? this.y2AxisDefinitionComponent.getAxisDefinition()
: undefined;
const config = new PlotConfig( const config = new PlotConfig(
this.query.query, this.query.query,
this.groupBy.map(o => o.name), this.groupBy.map((o) => o.name),
this.limitbycomponent.limitBy, this.limitbycomponent.limitBy,
this.limitbycomponent.limit, this.limitbycomponent.limit,
y1, y1,
y2, y2,
this.dateRangeAsString(), // dateRange this.datePicker.getDateValue(), // dateRange
aggregates, // aggregates aggregates, // aggregates
this.intervalUnit, this.intervalUnit,
this.intervalValue, this.intervalValue,
this.renderBarChartTickLabels, this.renderBarChartTickLabels,
); );
return config; return config;
} }
createPlotRequest(): PlotRequest { createPlotRequest(): PlotRequest {
const results = document.getElementById("results"); const results = document.getElementById("results");
const config = this.createPlotConfig(); const config = this.createPlotConfig();
const renderOptions : RenderOptionsMap = { const renderOptions: RenderOptionsMap = {
'main': new RenderOptions(results!.offsetHeight-1, results!.offsetWidth-1, false, true), "main": new RenderOptions(
'thumbnail': new RenderOptions(200, 300, false, false), results!.offsetHeight - 1,
results!.offsetWidth - 1,
true,
true,
),
"thumbnail": new RenderOptions(200, 300, false, false),
}; };
const request = new PlotRequest( const request = new PlotRequest(
(<any>window).submitterId, (<any> window).submitterId,
config, config,
renderOptions renderOptions,
); );
return request; return request;
} }
serializedConfig(): string {
try {
const config = this.createPlotConfig();
return JSON.stringify(config);
} catch (e) {
return "";
}
}
} }

View File

@@ -11,11 +11,15 @@
<mat-form-field class="pdb-form-mid"> <mat-form-field class="pdb-form-mid">
<mat-label>Y{{yIndex}}-Axis Unit:</mat-label> <mat-label>Y{{yIndex}}-Axis Unit:</mat-label>
<mat-select [(value)]="yAxisUnit"> <mat-select [(value)]="yAxisUnit">
<mat-optgroup label="numbers"> <mat-optgroup label="numbers">
<mat-option value="AUTOMATIC_NUMBER">auto (number)</mat-option> <mat-option value="AUTOMATIC_NUMBER">auto (number)</mat-option>
<mat-option value="NO_UNIT">no unit</mat-option> <mat-option value="NO_UNIT">no unit</mat-option>
</mat-optgroup> </mat-optgroup>
<mat-optgroup label="⸺time⸺"> <mat-optgroup label="—bytes—">
<mat-option value="AUTOMATIC_BYTES">auto (bytes)</mat-option>
<mat-option value="BYTES">bytes</mat-option>
</mat-optgroup>
<mat-optgroup label="—time—">
<mat-option value="AUTOMATIC_TIME">auto (time)</mat-option> <mat-option value="AUTOMATIC_TIME">auto (time)</mat-option>
<mat-option value="MILLISECONDS">millis</mat-option> <mat-option value="MILLISECONDS">millis</mat-option>
<mat-option value="SECONDS">seconds</mat-option> <mat-option value="SECONDS">seconds</mat-option>
@@ -25,11 +29,11 @@
</mat-optgroup> </mat-optgroup>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="yAxisUnit !== 'AUTOMATIC_TIME' && yAxisUnit !== 'AUTOMATIC_NUMBER'" class="pdb-form-number"> <mat-form-field *ngIf="yAxisUnit !== 'AUTOMATIC_TIME' && yAxisUnit !== 'AUTOMATIC_NUMBER' && yAxisUnit !== 'AUTOMATIC_BYTES'" class="pdb-form-number">
<mat-label>Min:</mat-label> <mat-label>Min:</mat-label>
<input matInput type="number" placeholder="Min" min="0" [(ngModel)]="minYValue"> <input matInput type="number" placeholder="Min" min="0" [(ngModel)]="minYValue">
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="yAxisUnit !== 'AUTOMATIC_TIME' && yAxisUnit !== 'AUTOMATIC_NUMBER'" class="pdb-form-number"> <mat-form-field *ngIf="yAxisUnit !== 'AUTOMATIC_TIME' && yAxisUnit !== 'AUTOMATIC_NUMBER' && yAxisUnit !== 'AUTOMATIC_BYTES'" class="pdb-form-number">
<mat-label>Max:</mat-label> <mat-label>Max:</mat-label>
<input matInput type="number" placeholder="Max" min="0" [(ngModel)]="maxYValue"> <input matInput type="number" placeholder="Max" min="0" [(ngModel)]="maxYValue">
</mat-form-field> </mat-form-field>

View File

@@ -8,8 +8,8 @@ describe('YAxisDefinitionComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ YAxisDefinitionComponent ] imports: [YAxisDefinitionComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@@ -1,10 +1,18 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { YAxisDefinition } from '../plot.service'; import { YAxisDefinition } from '../plot.service';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatSelect } from '@angular/material/select';
import { MatOption, MatOptgroup } from '@angular/material/core';
import { NgIf } from '@angular/common';
import { MatInput } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
@Component({ @Component({
selector: 'pdb-y-axis-definition', selector: 'pdb-y-axis-definition',
templateUrl: './y-axis-definition.component.html', templateUrl: './y-axis-definition.component.html',
styleUrls: ['./y-axis-definition.component.scss'] styleUrls: ['./y-axis-definition.component.scss'],
standalone: true,
imports: [MatFormField, MatLabel, MatSelect, MatOption, MatOptgroup, NgIf, MatInput, FormsModule]
}) })
export class YAxisDefinitionComponent { export class YAxisDefinitionComponent {

View File

@@ -1,410 +0,0 @@
.daterangepicker {
position: absolute;
color: inherit;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ddd;
width: 278px;
max-width: none;
padding: 0;
margin-top: 7px;
top: 100px;
left: 20px;
z-index: 3001;
display: none;
font-family: arial;
font-size: 15px;
line-height: 1em;
}
.daterangepicker:before, .daterangepicker:after {
position: absolute;
display: inline-block;
border-bottom-color: rgba(0, 0, 0, 0.2);
content: '';
}
.daterangepicker:before {
top: -7px;
border-right: 7px solid transparent;
border-left: 7px solid transparent;
border-bottom: 7px solid #ccc;
}
.daterangepicker:after {
top: -6px;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
border-left: 6px solid transparent;
}
.daterangepicker.opensleft:before {
right: 9px;
}
.daterangepicker.opensleft:after {
right: 10px;
}
.daterangepicker.openscenter:before {
left: 0;
right: 0;
width: 0;
margin-left: auto;
margin-right: auto;
}
.daterangepicker.openscenter:after {
left: 0;
right: 0;
width: 0;
margin-left: auto;
margin-right: auto;
}
.daterangepicker.opensright:before {
left: 9px;
}
.daterangepicker.opensright:after {
left: 10px;
}
.daterangepicker.drop-up {
margin-top: -7px;
}
.daterangepicker.drop-up:before {
top: initial;
bottom: -7px;
border-bottom: initial;
border-top: 7px solid #ccc;
}
.daterangepicker.drop-up:after {
top: initial;
bottom: -6px;
border-bottom: initial;
border-top: 6px solid #fff;
}
.daterangepicker.single .daterangepicker .ranges, .daterangepicker.single .drp-calendar {
float: none;
}
.daterangepicker.single .drp-selected {
display: none;
}
.daterangepicker.show-calendar .drp-calendar {
display: block;
}
.daterangepicker.show-calendar .drp-buttons {
display: block;
}
.daterangepicker.auto-apply .drp-buttons {
display: none;
}
.daterangepicker .drp-calendar {
display: none;
max-width: 270px;
}
.daterangepicker .drp-calendar.left {
padding: 8px 0 8px 8px;
}
.daterangepicker .drp-calendar.right {
padding: 8px;
}
.daterangepicker .drp-calendar.single .calendar-table {
border: none;
}
.daterangepicker .calendar-table .next span, .daterangepicker .calendar-table .prev span {
color: #fff;
border: solid black;
border-width: 0 2px 2px 0;
border-radius: 0;
display: inline-block;
padding: 3px;
}
.daterangepicker .calendar-table .next span {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
}
.daterangepicker .calendar-table .prev span {
transform: rotate(135deg);
-webkit-transform: rotate(135deg);
}
.daterangepicker .calendar-table th, .daterangepicker .calendar-table td {
white-space: nowrap;
text-align: center;
vertical-align: middle;
min-width: 32px;
width: 32px;
height: 24px;
line-height: 24px;
font-size: 12px;
border-radius: 4px;
border: 1px solid transparent;
white-space: nowrap;
cursor: pointer;
}
.daterangepicker .calendar-table {
border: 1px solid #fff;
border-radius: 4px;
background-color: #fff;
}
.daterangepicker .calendar-table table {
width: 100%;
margin: 0;
border-spacing: 0;
border-collapse: collapse;
}
.daterangepicker td.available:hover, .daterangepicker th.available:hover {
background-color: #eee;
border-color: transparent;
color: inherit;
}
.daterangepicker td.week, .daterangepicker th.week {
font-size: 80%;
color: #ccc;
}
.daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date {
background-color: #fff;
border-color: transparent;
color: #999;
}
.daterangepicker td.in-range {
background-color: #ebf4f8;
border-color: transparent;
color: #000;
border-radius: 0;
}
.daterangepicker td.start-date {
border-radius: 4px 0 0 4px;
}
.daterangepicker td.end-date {
border-radius: 0 4px 4px 0;
}
.daterangepicker td.start-date.end-date {
border-radius: 4px;
}
.daterangepicker td.active, .daterangepicker td.active:hover {
background-color: #357ebd;
border-color: transparent;
color: #fff;
}
.daterangepicker th.month {
width: auto;
}
.daterangepicker td.disabled, .daterangepicker option.disabled {
color: #999;
cursor: not-allowed;
text-decoration: line-through;
}
.daterangepicker select.monthselect, .daterangepicker select.yearselect {
font-size: 12px;
padding: 1px;
height: auto;
margin: 0;
cursor: default;
}
.daterangepicker select.monthselect {
margin-right: 2%;
width: 56%;
}
.daterangepicker select.yearselect {
width: 40%;
}
.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect {
width: 50px;
margin: 0 auto;
background: #eee;
border: 1px solid #eee;
padding: 2px;
outline: 0;
font-size: 12px;
}
.daterangepicker .calendar-time {
text-align: center;
margin: 4px auto 0 auto;
line-height: 30px;
position: relative;
}
.daterangepicker .calendar-time select.disabled {
color: #ccc;
cursor: not-allowed;
}
.daterangepicker .drp-buttons {
clear: both;
text-align: right;
padding: 8px;
border-top: 1px solid #ddd;
display: none;
line-height: 12px;
vertical-align: middle;
}
.daterangepicker .drp-selected {
display: inline-block;
font-size: 12px;
padding-right: 8px;
}
.daterangepicker .drp-buttons .btn {
margin-left: 8px;
font-size: 12px;
font-weight: bold;
padding: 4px 8px;
}
.daterangepicker.show-ranges.single.rtl .drp-calendar.left {
border-right: 1px solid #ddd;
}
.daterangepicker.show-ranges.single.ltr .drp-calendar.left {
border-left: 1px solid #ddd;
}
.daterangepicker.show-ranges.rtl .drp-calendar.right {
border-right: 1px solid #ddd;
}
.daterangepicker.show-ranges.ltr .drp-calendar.left {
border-left: 1px solid #ddd;
}
.daterangepicker .ranges {
float: none;
text-align: left;
margin: 0;
}
.daterangepicker.show-calendar .ranges {
margin-top: 8px;
}
.daterangepicker .ranges ul {
list-style: none;
margin: 0 auto;
padding: 0;
width: 100%;
}
.daterangepicker .ranges li {
font-size: 12px;
padding: 8px 12px;
cursor: pointer;
}
.daterangepicker .ranges li:hover {
background-color: #eee;
}
.daterangepicker .ranges li.active {
background-color: #08c;
color: #fff;
}
/* Larger Screen Styling */
@media (min-width: 564px) {
.daterangepicker {
width: auto;
}
.daterangepicker .ranges ul {
width: 140px;
}
.daterangepicker.single .ranges ul {
width: 100%;
}
.daterangepicker.single .drp-calendar.left {
clear: none;
}
.daterangepicker.single .ranges, .daterangepicker.single .drp-calendar {
float: left;
}
.daterangepicker {
direction: ltr;
text-align: left;
}
.daterangepicker .drp-calendar.left {
clear: left;
margin-right: 0;
}
.daterangepicker .drp-calendar.left .calendar-table {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.daterangepicker .drp-calendar.right {
margin-left: 0;
}
.daterangepicker .drp-calendar.right .calendar-table {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.daterangepicker .drp-calendar.left .calendar-table {
padding-right: 8px;
}
.daterangepicker .ranges, .daterangepicker .drp-calendar {
float: left;
}
}
@media (min-width: 730px) {
.daterangepicker .ranges {
width: auto;
}
.daterangepicker .ranges {
float: left;
}
.daterangepicker.rtl .ranges {
float: right;
}
.daterangepicker .drp-calendar.left {
clear: none !important;
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><rect fill="none" height="24" width="24"/><path d="M17,11v6.97l-5-2.14l-5,2.14V5h6V3H7C5.9,3,5,3.9,5,5v16l7-3l7,3V11H17z M21,7h-2v2h-2V7h-2V5h2V3h2v2h2V7z"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2zm0 15l-5-2.18L7 18V5h10v13z"/></svg>

After

Width:  |  Height:  |  Size: 219 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15 7v12.97l-4.21-1.81-.79-.34-.79.34L5 19.97V7h10m4-6H8.99C7.89 1 7 1.9 7 3h10c1.1 0 2 .9 2 2v13l2 1V3c0-1.1-.9-2-2-2zm-4 4H5c-1.1 0-2 .9-2 2v16l7-3 7 3V7c0-1.1-.9-2-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 32 32">
<g transform="translate(0.5,0.5)" style="fill:black">
<circle cx="4" cy="9" r="2"/>
<circle cx="12" cy="9" r="2"/>
<circle cx="20" cy="9" r="2"/>
<circle cx="28" cy="9" r="2"/>
<circle cx="4" cy="15" r="2"/>
<circle cx="12" cy="15" r="2"/>
<circle cx="20" cy="15" r="2"/>
<circle cx="28" cy="15" r="2"/>
<circle cx="4" cy="21" r="2"/>
<circle cx="12" cy="21" r="2"/>
<circle cx="20" cy="21" r="2"/>
<circle cx="28" cy="21" r="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 534 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="-30 -30 700 700">
<path d="M-.012 65.611h640.024v508.778H-.012V65.611zm180.026 132.273c22.996 0 41.635 18.638 41.635 41.634 0 22.997-18.638 41.635-41.635 41.635-22.996 0-41.634-18.638-41.634-41.635 0-22.996 18.638-41.634 41.634-41.634zm175.207 178.679l83.269-143.978 88.466 223.763h-412.86v-27.756l34.702-1.725 34.69-85.005 17.338 60.722h52.052l45.095-116.222 57.248 90.201zM47.528 107.764h544.944v424.47H47.528v-424.47z"/>
<line x1="10" y1="630" x2="630" y2="10" style="stroke:#000; stroke-width: 60px; stroke-linecap: round;" />
</svg>

After

Width:  |  Height:  |  Size: 758 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title>ionicons-v5-g</title><polyline points="176 112 256 32 336 112" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="255.98" y1="32" x2="256" y2="480" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><polyline points="176 400 256 480 336 400" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><polyline points="400 176 480 256 400 336" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><polyline points="112 176 32 256 112 336" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="32" y1="256" x2="480" y2="256" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>

After

Width:  |  Height:  |  Size: 928 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title>ionicons-v5-c</title><polyline points="304 96 416 96 416 208" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="405.77" y1="106.2" x2="111.98" y2="400.02" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><polyline points="208 416 96 416 96 304" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title>ionicons-v5-p</title><path d="M380.93,57.37A32,32,0,0,0,358.3,48H94.22A46.21,46.21,0,0,0,48,94.22V417.78A46.21,46.21,0,0,0,94.22,464H417.78A46.36,46.36,0,0,0,464,417.78V153.7a32,32,0,0,0-9.37-22.63ZM256,416a64,64,0,1,1,64-64A63.92,63.92,0,0,1,256,416Zm48-224H112a16,16,0,0,1-16-16V112a16,16,0,0,1,16-16H304a16,16,0,0,1,16,16v64A16,16,0,0,1,304,192Z" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>

After

Width:  |  Height:  |  Size: 542 B

File diff suppressed because it is too large Load Diff

View File

@@ -7,35 +7,27 @@ var invaders_stepSize = 5;
var invaders_margin = 30; var invaders_margin = 30;
var invaders_city_height = 75; var invaders_city_height = 75;
var invaders_pause = true; var invaders_pause = true;
var invaders_parentDivId = 'invaders_area'; var invaders_area = 'invaders_area';
var invaders_kills = 0; var invaders_kills = 0;
var invaders_points = 0; var invaders_points = 0;
var invaders_points_kill = 10; var invaders_points_kill = 10;
var invaders_points_lost = -50; var invaders_points_lost = -50;
var invaders_game_over = false; var invaders_game_over = false;
var invaders_loop_count = 0; var invaders_loop_count = 0;
var invaders_parentDivId = "invaders";
function initInvaders(parentDivId) { function initInvaders(parentDivId) {
invaders_parentDivId = parentDivId;
// create a copy of the parentDiv
// and set it at the exact same position $('body').append('<div id="'+invaders_area+'"><div id="invaders_points">Points: 0</div><div id="invaders_kills">Kills: 0</div><div id="invaders_game_over"><div class="invader_notify">Game Over</div></div></div>');
var parent = $('#'+parentDivId);
var height = parent.height();
var width = parent.width();
$('body').append('<div id="'+invaders_parentDivId+'"><div id="invaders_points">Points: 0</div><div id="invaders_kills">Kills: 0</div><div id="invaders_game_over"><div class="invader_notify">Game Over</div></div></div>');
$('#'+invaders_parentDivId).offset({ top: parent.offset().top, left: parent.offset().left})
$('#'+invaders_parentDivId).height(parent.height());
$('#'+invaders_parentDivId).width(parent.width());
$('#'+invaders_parentDivId)
$('.invader_notify').click(function() { $('.invader_notify').click(function() {
// restart the game // restart the game
$('#'+invaders_parentDivId).remove(); $('#'+invaders_area).remove();
initInvaders(parentDivId); initInvaders(invaders_parentDivId);
invaders_game_over = false; invaders_game_over = false;
invaders_kills = 0; invaders_kills = 0;
invaders_points = 0; invaders_points = 0;
startInvaders(); startInvaders();
}); });
@@ -49,7 +41,7 @@ function gameOver() {
} }
function pauseInvaders() { function pauseInvaders() {
$('#'+invaders_parentDivId).hide(); $('#'+invaders_area).hide();
clearIntervals(); clearIntervals();
} }
@@ -59,16 +51,26 @@ function clearIntervals() {
} }
function startInvaders() { function startInvaders() {
// move invaders_area to the same position as the parent div
const parent = $('#'+invaders_parentDivId);
const height = parent.height();
const width = parent.width();
const area = document.getElementById(invaders_area);
area.style.top=parent.offset().top+"px";
area.style.left=parent.offset().left+"px";
area.style.height=parent.height()+"px";
area.style.width=parent.width()+"px";
$('#'+invaders_parentDivId).show(); $('#'+invaders_area).show();
if (!invaders_game_over) { if (!invaders_game_over) {
if (invaders_count == 0) { if (invaders_count == 0) {
addInvader(invaders_parentDivId); addInvader(invaders_area);
} }
clearIntervals(); clearIntervals();
invaders_game_move=window.setInterval("moveRandomly('"+invaders_parentDivId+"')",100); invaders_game_move=window.setInterval("moveRandomly('"+invaders_area+"')",100);
invaders_game_new=window.setInterval("addInvader()", 1000); invaders_game_new=window.setInterval("addInvader()", 1000);
} }
} }
@@ -77,7 +79,7 @@ function startInvaders() {
function addInvader() function addInvader()
{ {
var id = 'invader_' + invaders_count++; var id = 'invader_' + invaders_count++;
var parent = $('#'+invaders_parentDivId); var parent = $('#'+invaders_area);
var height = parent.height(); var height = parent.height();
var width = parent.width(); var width = parent.width();
var top = 10; // start at the top var top = 10; // start at the top
@@ -114,8 +116,8 @@ function moveRandomly(parentDivId)
var top = invader.position().top; var top = invader.position().top;
var left = invader.position().left; var left = invader.position().left;
var parent = $('#'+parentDivId); var parent = $('#'+parentDivId);
var minTop = parent.position().top; var minTop = parent.position().top;
var maxTop = parent.height(); var maxTop = parent.height();
var width = parent.width(); var width = parent.width();

File diff suppressed because one or more lines are too long

View File

@@ -8,49 +8,14 @@
<link rel="icon" type="image/x-icon" href="/assets/img/favicon.png"> <link rel="icon" type="image/x-icon" href="/assets/img/favicon.png">
<script type="text/javascript" src="/assets/js/jquery-3.6.0.min.js"></script> <script type="text/javascript" src="/assets/js/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="/assets/js/moment-2.29.1.min.js"></script>
<script type="text/javascript" src="/assets/js/daterangepicker-3.1.js"></script>
<script type="text/javascript" src="/assets/js/invaders.js"></script> <script type="text/javascript" src="/assets/js/invaders.js"></script>
<link rel="stylesheet" type="text/css" href="/assets/css/daterangepicker-3.1.css" />
<link rel="stylesheet" type="text/css" href="/assets/css/invaders.css" /> <link rel="stylesheet" type="text/css" href="/assets/css/invaders.css" />
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>
<script> <script>
function initDatePicker() {
$('input[name="dates"]').daterangepicker({
timePicker: true,
minDate: "2017-01-01",
maxDate: "2029-12-31",
maxYear: parseInt(moment().format('YYYY'),10),
timePicker24Hour: true,
timePickerSeconds: true,
showDropdowns: true, // drop downs for selecting year and month
locale: {
format: 'YYYY-MM-DD HH:mm:ss',
"firstDay": 1 // monday is the first day of the week
},
ranges: {
'Today': [moment().startOf('day'), moment().endOf('day')],
'Yesterday': [moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').endOf('day')],
'Last 7 Days': [moment().subtract(6, 'days').startOf('day'), moment().endOf('day')],
'This Week': [moment().startOf('week'), moment().endOf('day')],
'Last Week': [moment().subtract(7, 'days').startOf('week'), moment().subtract(7, 'days').endOf('week')],
'Last 30 Days': [moment().subtract(29, 'days').startOf('day'), moment().endOf('day')],
'This Month': [moment().startOf('month'), moment().endOf('month').endOf('day')],
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')],
'Last 3 Months': [moment().subtract(3, 'month').startOf('month'),moment().endOf('month').endOf('day')],
'This Year': [moment().startOf('year'),moment().endOf('month').endOf('day')],
'Last Year': [moment().subtract(1, 'year').startOf('year'),moment().subtract(1, 'year').endOf('year')],
},
});
}
$( document ).ready(function() { $( document ).ready(function() {
//initDatePicker();
initInvaders('results'); initInvaders('results');
document.addEventListener("invadersPause", function(event) { document.addEventListener("invadersPause", function(event) {
pauseInvaders(); pauseInvaders();

View File

@@ -1,14 +1,48 @@
import { enableProdMode } from '@angular/core'; import { enableProdMode, importProvidersFrom } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
import { MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { MarkdownModule } from 'ngx-markdown';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { AppComponent } from './app/app.component';
import { provideRouter, Routes, withDebugTracing } from '@angular/router';
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode();
} }
(<any>window).submitterId = crypto.randomUUID(); (<any>window).randomId = () => {
return Math.random().toString(36).replace('0.', '') + Math.random().toString(36).replace('0.', '');
};
platformBrowserDynamic().bootstrapModule(AppModule) (<any>window).submitterId = (<any>window).randomId();
const routes: Routes = [
{ path: "", loadComponent: () => import("./app/main-page/main-page.component").then( m => m.MainPageComponent) },
{ path: "vis", loadComponent: () => import("./app/visualization-page/visualization-page.component").then(m => m.VisualizationPageComponent) },
{ path: "dashboard", loadComponent: () => import("./app/dashboard-page/dashboard-page.component").then(m => m.DashboardPageComponent) },
{ path: "dashboard/:id", loadComponent: () => import("./app/dashboard-page/dashboard/dashboard.component").then(m => m.DashboardComponent) },
{ path: "upload", loadComponent: () => import("./app/upload-page/upload-page.component").then(m => m.UploadPageComponent) },
{ path: "grid", loadComponent: () => import("./app/customizable-grid/customizable-grid.component").then(m => m.CustomizableGridComponent) },
{ path: "help", loadComponent: () => import("./app/help-page/help-page.component").then(m => m.HelpPageComponent) },
];
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withDebugTracing()),
importProvidersFrom(
MarkdownModule.forRoot(),
),
{
provide: MAT_DIALOG_DEFAULT_OPTIONS,
useValue: { hasBackdrop: true },
},
provideHttpClient(withInterceptorsFromDi()),
provideAnimations()
]
})
.catch(err => console.error(err)); .catch(err => console.error(err));

View File

@@ -14,25 +14,21 @@
// If you don't need the default component typographies but still want the hierarchy styles, // If you don't need the default component typographies but still want the hierarchy styles,
// you can delete this line and instead use: // you can delete this line and instead use:
// `@include mat.legacy-typography-hierarchy(mat.define-typography-config());` // `@include mat.legacy-typography-hierarchy(mat.define-typography-config());`
/* TODO(mdc-migration): Remove all-legacy-component-typographies once all legacy components are migrated*/
@include mat.all-legacy-component-typographies();
@include mat.all-component-typographies(); @include mat.all-component-typographies();
/* TODO(mdc-migration): Remove legacy-core once all legacy components are migrated*/
@include mat.legacy-core();
@include mat.core(); @include mat.core();
// Define the palettes for your theme using the Material Design palettes available in palette.scss // Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker // (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/ // hue. Available color palettes: https://material.io/design/color/
$candy-app-primary: mat.define-palette(mat.$blue-palette); $candy-app-primary: mat.m2-define-palette(mat.$m2-blue-palette);
$candy-app-accent: mat.define-palette(mat.$blue-palette, A200, A100, A400); $candy-app-accent: mat.m2-define-palette(mat.$m2-blue-palette, A200, A100, A400);
// The warn palette is optional (defaults to red). // The warn palette is optional (defaults to red).
$candy-app-warn: mat.define-palette(mat.$red-palette); $candy-app-warn: mat.m2-define-palette(mat.$m2-red-palette);
// Create the theme object. A theme consists of configurations for individual // Create the theme object. A theme consists of configurations for individual
// theming systems such as "color" or "typography". // theming systems such as "color" or "typography".
$candy-app-theme: mat.define-light-theme(( $candy-app-theme: mat.m2-define-light-theme((
color: ( color: (
primary: $candy-app-primary, primary: $candy-app-primary,
accent: $candy-app-accent, accent: $candy-app-accent,
@@ -45,8 +41,6 @@ $candy-app-theme: mat.define-light-theme((
// Include theme styles for core and each component used in your app. // Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component // Alternatively, you can import and @include the theme mixins for each component
// that you are using. // that you are using.
/* TODO(mdc-migration): Remove all-legacy-component-themes once all legacy components are migrated*/
@include mat.all-legacy-component-themes($candy-app-theme);
@include mat.all-component-themes($candy-app-theme); @include mat.all-component-themes($candy-app-theme);
@@ -71,6 +65,8 @@ grey
*/ */
$background-color: #CBD7F4; $background-color: #CBD7F4;
.mat-mdc-option span.mdc-list-item__primary-text, .mat-mdc-option span.mdc-list-item__primary-text,
.mdc-list-item__primary-text { .mdc-list-item__primary-text {
font-size: 1rem; font-size: 1rem;
@@ -84,6 +80,12 @@ mat-form-field .mat-mdc-option span.mdc-list-item__primary-text{
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
// set colors for flat buttons (directive mat-flat-button)
// make them blue with white text when active and transparent with the default text color of light grey when disabled
--mdc-filled-button-label-text-color: white;
--mdc-filled-button-container-color: #005cbb;
--mdc-filled-button-disabled-container-color: none;
} }
h1 { h1 {
@@ -97,6 +99,10 @@ h2 {
margin-block-end: 0.83rem; margin-block-end: 0.83rem;
} }
.hidden {
visibility: hidden;
}
.icon-inline { .icon-inline {
width: 1em; width: 1em;
height: 1em; height: 1em;
@@ -107,26 +113,21 @@ button[disabled] .icon-inline {
opacity: 0.5; opacity: 0.5;
} }
button.save-button { .icon-tiny {
background-color: #ff9900; width: 1em;
height: 1em;
} }
.icon-small { .icon-small {
width: 1.5em; width: 1.5em;
height: 1.5em; height: 1.5em;
} }
.icon-small:hover {
background-color: #eee;
}
.icon-middle { .icon-middle {
width: 2.5em; width: 2.5em;
height: 2.5em; height: 2.5em;
margin: 0.2em; margin: 0.2em;
} }
.icon-middle:hover {
background-color: #eee;
}
.icon-large { .icon-large {
width: 8em; width: 8em;
@@ -145,8 +146,6 @@ button.save-button {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
a ,a:visited { a ,a:visited {
color: blue; color: blue;
text-decoration: none; text-decoration: none;
@@ -178,6 +177,9 @@ a.external-link:after {
mat-form-field.pdb-form-full-width { mat-form-field.pdb-form-full-width {
width: 100%; width: 100%;
} }
mat-form-field.pdb-form-number-small {
width: 4.5em;
}
mat-form-field.pdb-form-number { mat-form-field.pdb-form-number {
width: 5.5em; width: 5.5em;
} }
@@ -190,13 +192,14 @@ mat-form-field.pdb-form-mid {
mat-form-field.pdb-form-wide { mat-form-field.pdb-form-wide {
width: 10em; width: 10em;
} }
.mat-mdc-form-field-subscript-wrapper { pdb-visualization-page .mat-mdc-form-field-subscript-wrapper,
display: none; app-add-text-dialog .mat-mdc-form-field-subscript-wrapper {
display: none;/**/
} }
.errorPanel { .errorPanel {
padding: 1ex; padding: 1ex;
background-color: map-get(mat.$red-palette, 100); background-color: map-get(mat.$m2-red-palette, 100);
border-radius: 5px; border-radius: 5px;
} }
@@ -207,3 +210,73 @@ mat-form-field.pdb-form-wide {
top: 0.2em; top: 0.2em;
line-height: 1em; line-height: 1em;
} }
a.external-link:after {
content: "";
display: inline-block;
background: url("assets/img/external-link.svg") no-repeat;
background-size: 1em;
width: 1em;
height: 1em;
margin-left: 0.3em;
vertical-align: text-top;
}
/* styles for markdown*/
markdown blockquote {
border-left: 3px grey solid;
display: block;
margin: 1em 0 1em 2em;
padding: 0.5em 0 0.5em 0.5em;
}
markdown table {
border-collapse: collapse;
}
markdown table th, markdown table td {
border: solid 1px black;
padding: 0.4em;
}
markdown thead {
border-bottom: 2px black solid;
}
markdown tfoot {
border-top: 2px black solid;
}
markdown pre {
font-family: monospace;
}
.plot-details-plotType {
background-image: url(/assets/img/pointTypes.png);
width: 9px;
height: 7px;
transform: scale(1.5);
}
.plot-details-plotType_0 {background-position-x: 0px;}
.plot-details-plotType_1 {background-position-x: -10px;}
.plot-details-plotType_2 {background-position-x: -20px;}
.plot-details-plotType_3 {background-position-x: -30px;}
.plot-details-plotType_4 {background-position-x: -40px;}
.plot-details-plotType_5 {background-position-x: -50px;}
.plot-details-plotType_6 {background-position-x: -60px;}
.plot-details-plotType_7 {background-position-x: -70px;}
.plot-details-plotType_8 {background-position-x: -80px;}
.plot-details-plotType_9 {background-position-x: -90px;}
.plot-details-plotType_10 {background-position-x:-100px;}
.plot-details-plotType_11 {background-position-x:-110px;}
.plot-details-plotType_12 {background-position-x:-120px;}
.plot-details-plotType_0051c2 {background-position-y: 0px;}
.plot-details-plotType_bf8300 {background-position-y: -8px;}
.plot-details-plotType_9400d3 {background-position-y: -16px;}
.plot-details-plotType_00c254 {background-position-y: -24px;}
.plot-details-plotType_e6e600 {background-position-y: -32px;}
.plot-details-plotType_e51e10 {background-position-y: -40px;}
.plot-details-plotType_57a1c2 {background-position-y: -48px;}
.plot-details-plotType_bd36c2 {background-position-y: -56px;}

View File

@@ -5,6 +5,7 @@
"baseUrl": "./", "baseUrl": "./",
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"strict": true, "strict": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
@@ -12,7 +13,6 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,

View File

@@ -0,0 +1,143 @@
package org.lucares.pdb.plot.api;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.lucares.pdb.api.DateTimeRange;
import org.lucares.utils.Preconditions;
public class DateTimeRangeParser {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static DateTimeRange parse(final OffsetDateTime offsetTime, final String datePeriod) {
final String[] startEnd = datePeriod.split(Pattern.quote("/"));
final String start = startEnd[0];
final String end = startEnd[1];
final OffsetDateTime startTime = parseInternal(offsetTime, start);
final OffsetDateTime endTime = parseInternal(offsetTime, end);
return new DateTimeRange(startTime, endTime);
}
private static OffsetDateTime parseInternal(final OffsetDateTime offsetTime, final String timeDefinition) {
final Pattern regex = Pattern.compile("(?<beginEnd>[BE])(?<amountUnit>(\\-?[0-9]*[mHDWMY])+)",
Pattern.MULTILINE);
final Matcher matcher = regex.matcher(timeDefinition);
if (matcher.matches()) {
final String beginEnd = matcher.group("beginEnd");
final boolean begin = "B".equals(beginEnd);
OffsetDateTime result = offsetTime;
final String amountUnitString = matcher.group("amountUnit");
final Pattern regexAmountUnit = Pattern.compile("(?<amount>\\-?[0-9]*)(?<unit>[mHDWMY])");
final Matcher m = regexAmountUnit.matcher(amountUnitString);
while (m.find()) {
final String amountString = m.group("amount");
final String unitString = m.group("unit");
final int amount = amountString.equals("") ? 0 : Integer.parseInt(amountString);
switch (unitString) {
case "m": {
final ChronoUnit unit = ChronoUnit.MINUTES;
if (begin) {
result = result.plus(amount, unit).truncatedTo(unit);
} else {
result = result.plus(amount + 1, unit).truncatedTo(unit).minusSeconds(1);
}
break;
}
case "H": {
final ChronoUnit unit = ChronoUnit.HOURS;
if (begin) {
result = result.plus(amount, unit).truncatedTo(unit);
} else {
result = result.plus(amount + 1, unit).truncatedTo(unit).minusSeconds(1);
}
break;
}
case "D": {
final ChronoUnit unit = ChronoUnit.DAYS;
if (begin) {
result = result.plus(amount, unit).truncatedTo(unit);
} else {
result = result.plus(amount + 1, unit).truncatedTo(unit).minusSeconds(1);
}
break;
}
case "W": {
final DayOfWeek firstDayOfWeek = DayOfWeek.MONDAY;
final DayOfWeek lastDayOfWeek = DayOfWeek
.of(((firstDayOfWeek.getValue() - 1 + 6) % DayOfWeek.values().length) + 1); // weird
// computation,
// because
// DayOfWeek
// goes from 1
// to 7
final ChronoUnit unit = ChronoUnit.WEEKS;
if (begin) {
result = result.plus(amount, unit).with(TemporalAdjusters.previousOrSame(firstDayOfWeek))
.truncatedTo(ChronoUnit.DAYS);
} else {
result = result.plus(amount, unit).with(TemporalAdjusters.nextOrSame(lastDayOfWeek))
.plus(1, ChronoUnit.DAYS).truncatedTo(ChronoUnit.DAYS).minusSeconds(1);
}
break;
}
case "M": {
final ChronoUnit unit = ChronoUnit.MONTHS;
if (begin) {
result = result.plus(amount, unit).truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1);
} else {
result = result.plus(amount, unit).truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1)
.plus(1, ChronoUnit.MONTHS).minusSeconds(1);
}
break;
}
case "Y": {
final ChronoUnit unit = ChronoUnit.YEARS;
if (begin) {
result = result.plus(amount, unit).truncatedTo(ChronoUnit.DAYS).withDayOfYear(1);
} else {
result = result.plus(amount, unit).truncatedTo(ChronoUnit.DAYS).withDayOfYear(1)
.plus(1, ChronoUnit.YEARS).minusSeconds(1);
}
break;
}
default:
throw new IllegalArgumentException("Unexpected value: " + unitString);
}
}
return result;
}
throw new IllegalArgumentException("invalid input: " + timeDefinition);
}
public static DateTimeRange parseAbsolute(final String dateRangeAsString) {
final String[] startEnd = dateRangeAsString.split(Pattern.quote(" - "));
Preconditions.checkEqual(startEnd.length, 2, "invalid date range: ''{0}''", dateRangeAsString);
final String startString = startEnd[0];
final String endString = startEnd[1];
final OffsetDateTime start = LocalDateTime.parse(startString, DATE_FORMAT).atOffset(ZoneOffset.UTC);
final OffsetDateTime end = LocalDateTime.parse(endString, DATE_FORMAT).atOffset(ZoneOffset.UTC);
return new DateTimeRange(start, end);
}
}

View File

@@ -0,0 +1,38 @@
package org.lucares.pdb.plot.api;
public class DateValue {
public enum DateType {
QUICK, RELATIVE, ABSOLUTE
}
private DateType type;
private String display;
private String value;
public DateType getType() {
return type;
}
public void setType(final DateType type) {
this.type = type;
}
public String getDisplay() {
return display;
}
public void setDisplay(final String display) {
this.display = display;
}
public String getValue() {
return value;
}
public void setValue(final String value) {
this.value = value;
}
}

View File

@@ -1,23 +1,19 @@
package org.lucares.pdb.plot.api; package org.lucares.pdb.plot.api;
import java.time.LocalDateTime;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.regex.Pattern;
import org.lucares.pdb.api.DateTimeRange; import org.lucares.pdb.api.DateTimeRange;
import org.lucares.recommind.logs.GnuplotAxis; import org.lucares.recommind.logs.GnuplotAxis;
import org.lucares.recommind.logs.GnuplotSettings; import org.lucares.recommind.logs.GnuplotSettings;
import org.lucares.utils.Preconditions;
public class PlotSettings { public class PlotSettings {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String query; private String query;
@@ -27,7 +23,7 @@ public class PlotSettings {
private int limit; private int limit;
private String dateRangeAsString; private DateValue dateValue;
private YAxisDefinition y1; private YAxisDefinition y1;
private YAxisDefinition y2; private YAxisDefinition y2;
@@ -80,30 +76,32 @@ public class PlotSettings {
this.limit = limit; this.limit = limit;
} }
public String getDateRange() { public DateValue getDateRange() {
return dateRangeAsString; return dateValue;
} }
public void setDateRange(final String dateRangeAsString) { public void setDateRange(final DateValue dateValue) {
this.dateRangeAsString = dateRangeAsString; this.dateValue = dateValue;
} }
public DateTimeRange dateRange() { public DateTimeRange dateRange() {
final String[] startEnd = dateRangeAsString.split(Pattern.quote(" - ")); switch (this.dateValue.getType()) {
Preconditions.checkEqual(startEnd.length, 2, "invalid date range: ''{0}''", dateRangeAsString); case RELATIVE:
case QUICK:
final OffsetDateTime startDate = LocalDateTime.parse(startEnd[0], DATE_FORMAT).atOffset(ZoneOffset.UTC); final DateTimeRange dateTimeRange = DateTimeRangeParser.parse(OffsetDateTime.now(), dateValue.getValue());
final OffsetDateTime endDate = LocalDateTime.parse(startEnd[1], DATE_FORMAT).atOffset(ZoneOffset.UTC); return dateTimeRange;
case ABSOLUTE:
return new DateTimeRange(startDate, endDate); return DateTimeRangeParser.parseAbsolute(dateValue.getValue());
}
throw new UnsupportedOperationException();
} }
@Override @Override
public String toString() { public String toString() {
return "PlotSettings [query=" + query + ", groupBy=" + groupBy + ", limitBy=" + limitBy + ", limit=" + limit return "PlotSettings [query=" + query + ", groupBy=" + groupBy + ", limitBy=" + limitBy + ", limit=" + limit
+ ", dateRangeAsString=" + dateRangeAsString + ", y1=" + y1 + " y2=" + y2 + ", aggregates=" + aggregates + ", dateRangeAsString=" + dateValue + ", y1=" + y1 + " y2=" + y2 + ", aggregates=" + aggregates
+ ", renders=" + renders + "]"; + ", renders=" + renders + "]";
} }

View File

@@ -7,6 +7,8 @@ public enum RangeUnit {
NO_UNIT(false, Type.Number, "Value"), NO_UNIT(false, Type.Number, "Value"),
AUTOMATIC_BYTES(true, Type.Number, "Value"),
BYTES(false, Type.Number, "Value"), BYTES(false, Type.Number, "Value"),
AUTOMATIC_TIME(true, Type.Duration, "Duration"), AUTOMATIC_TIME(true, Type.Duration, "Duration"),
@@ -43,6 +45,10 @@ public enum RangeUnit {
return type == Type.Number || type == Type.HistogramCount; return type == Type.Number || type == Type.HistogramCount;
} }
public boolean isBytes() {
return this == BYTES || this == AUTOMATIC_BYTES;
}
public String getLabel() { public String getLabel() {
return axisLabel; return axisLabel;
} }
@@ -51,11 +57,9 @@ public enum RangeUnit {
return type; return type;
} }
public int valueForUnit(final int value) { public long valueForUnit(final long value) {
switch (this) { switch (this) {
case AUTOMATIC_NUMBER:
return Integer.MAX_VALUE;
case NO_UNIT: case NO_UNIT:
case BYTES: case BYTES:
return value; return value;
@@ -69,10 +73,12 @@ public enum RangeUnit {
return value * 60 * 60 * 1000; return value * 60 * 60 * 1000;
case DAYS: case DAYS:
return value * 24 * 60 * 60 * 1000; return value * 24 * 60 * 60 * 1000;
case AUTOMATIC_NUMBER:
case AUTOMATIC_TIME: case AUTOMATIC_TIME:
return Integer.MAX_VALUE; case AUTOMATIC_BYTES:
return Long.MAX_VALUE;
} }
return Integer.MAX_VALUE; return Long.MAX_VALUE;
} }
} }

View File

@@ -3,7 +3,7 @@ package org.lucares.pdb.plot.api;
public class RenderOptions { public class RenderOptions {
private int height; private int height;
private int width; private int width;
private boolean keyOutside; private boolean showKey;
private boolean renderLabels; private boolean renderLabels;
public int getHeight() { public int getHeight() {
@@ -22,12 +22,12 @@ public class RenderOptions {
this.width = width; this.width = width;
} }
public boolean isKeyOutside() { public boolean isShowKey() {
return keyOutside; return showKey;
} }
public void setKeyOutside(final boolean keyOutside) { public void setShowKey(final boolean showKey) {
this.keyOutside = keyOutside; this.showKey = showKey;
} }
public boolean isRenderLabels() { public boolean isRenderLabels() {

Some files were not shown because too many files have changed in this diff Show More