diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
index da8bf17c0ccf..daae91807157 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
@@ -254,6 +254,13 @@
services.kanata.
+
+
+ karma,
+ an alert dashboard for Prometheus Alertmanager. Available as
+ services.karma
+
+
languagetool,
diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md
index 68b46a7e45ab..821345e89254 100644
--- a/nixos/doc/manual/release-notes/rl-2211.section.md
+++ b/nixos/doc/manual/release-notes/rl-2211.section.md
@@ -91,6 +91,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [kanata](https://github.com/jtroo/kanata), a tool to improve keyboard comfort and usability with advanced customization.
Available as [services.kanata](options.html#opt-services.kanata.enable).
+- [karma](https://github.com/prymitive/karma), an alert dashboard for Prometheus Alertmanager. Available as [services.karma](options.html#opt-services.karma.enable)
+
- [languagetool](https://languagetool.org/), a multilingual grammar, style, and spell checker.
Available as [services.languagetool](options.html#opt-services.languagetool.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index c400fadef3a2..406bb71da8ce 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -684,6 +684,7 @@
./services/monitoring/heapster.nix
./services/monitoring/incron.nix
./services/monitoring/kapacitor.nix
+ ./services/monitoring/karma.nix
./services/monitoring/kthxbye.nix
./services/monitoring/loki.nix
./services/monitoring/longview.nix
diff --git a/nixos/modules/services/monitoring/karma.nix b/nixos/modules/services/monitoring/karma.nix
new file mode 100644
index 000000000000..85dbc81f443f
--- /dev/null
+++ b/nixos/modules/services/monitoring/karma.nix
@@ -0,0 +1,128 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+ cfg = config.services.karma;
+ yaml = pkgs.formats.yaml { };
+in
+{
+ options.services.karma = {
+ enable = mkEnableOption (mdDoc "the Karma dashboard service");
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.karma;
+ defaultText = literalExpression "pkgs.karma";
+ description = mdDoc ''
+ The Karma package that should be used.
+ '';
+ };
+
+ configFile = mkOption {
+ type = types.path;
+ default = yaml.generate "karma.yaml" cfg.settings;
+ defaultText = "A configuration file generated from the provided nix attributes settings option.";
+ description = mdDoc ''
+ A YAML config file which can be used to configure karma instead of the nix-generated file.
+ '';
+ example = "/etc/karma/karma.conf";
+ };
+
+ environment = mkOption {
+ type = with types; attrsOf str;
+ default = {};
+ description = mdDoc ''
+ Additional environment variables to provide to karma.
+ '';
+ example = {
+ ALERTMANAGER_URI = "https://alertmanager.example.com";
+ ALERTMANAGER_NAME= "single";
+ };
+ };
+
+ openFirewall = mkOption {
+ type = types.bool;
+ default = false;
+ description = mdDoc ''
+ Whether to open ports in the firewall needed for karma to function.
+ '';
+ };
+
+ extraOptions = mkOption {
+ type = with types; listOf str;
+ default = [];
+ description = mdDoc ''
+ Extra command line options.
+ '';
+ example = [
+ "--alertmanager.timeout 10s"
+ ];
+ };
+
+ settings = mkOption {
+ type = types.submodule {
+ freeformType = yaml.type;
+
+ options.listen = {
+ address = mkOption {
+ type = types.str;
+ default = "127.0.0.1";
+ description = mdDoc ''
+ Hostname or IP to listen on.
+ '';
+ example = "[::]";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 8080;
+ description = mdDoc ''
+ HTTP port to listen on.
+ '';
+ example = 8182;
+ };
+ };
+ };
+ default = {
+ listen = {
+ address = "127.0.0.1";
+ };
+ };
+ description = mdDoc ''
+ Karma dashboard configuration as nix attributes.
+
+ Reference:
+ '';
+ example = {
+ listen = {
+ address = "192.168.1.4";
+ port = "8000";
+ prefix = "/dashboard";
+ };
+ alertmanager = {
+ interval = "15s";
+ servers = [
+ {
+ name = "prod";
+ uri = "http://alertmanager.example.com";
+ }
+ ];
+ };
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.services.karma = {
+ description = "Alert dashboard for Prometheus Alertmanager";
+ wantedBy = [ "multi-user.target" ];
+ environment = cfg.environment;
+ serviceConfig = {
+ Type = "simple";
+ DynamicUser = true;
+ Restart = "on-failure";
+ ExecStart = "${pkgs.karma}/bin/karma --config.file ${cfg.configFile} ${concatStringsSep " " cfg.extraOptions}";
+ };
+ };
+ networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.listen.port ];
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 0fc08e841ec0..32342510f70f 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -299,6 +299,7 @@ in {
k3s = handleTest ./k3s {};
kafka = handleTest ./kafka.nix {};
kanidm = handleTest ./kanidm.nix {};
+ karma = handleTest ./karma.nix {};
kbd-setfont-decompress = handleTest ./kbd-setfont-decompress.nix {};
kbd-update-search-paths-patch = handleTest ./kbd-update-search-paths-patch.nix {};
kea = handleTest ./kea.nix {};
diff --git a/nixos/tests/karma.nix b/nixos/tests/karma.nix
new file mode 100644
index 000000000000..5ac2983b8aa3
--- /dev/null
+++ b/nixos/tests/karma.nix
@@ -0,0 +1,84 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+ name = "karma";
+ nodes = {
+ server = { ... }: {
+ services.prometheus.alertmanager = {
+ enable = true;
+ logLevel = "debug";
+ port = 9093;
+ openFirewall = true;
+ configuration = {
+ global = {
+ resolve_timeout = "1m";
+ };
+ route = {
+ # Root route node
+ receiver = "test";
+ group_by = ["..."];
+ continue = false;
+ group_wait = "1s";
+ group_interval="15s";
+ repeat_interval = "24h";
+ };
+ receivers = [
+ {
+ name = "test";
+ webhook_configs = [
+ {
+ url = "http://localhost:1234";
+ send_resolved = true;
+ max_alerts = 0;
+ }
+ ];
+ }
+ ];
+ };
+ };
+ services.karma = {
+ enable = true;
+ openFirewall = true;
+ settings = {
+ listen = {
+ address = "0.0.0.0";
+ port = 8081;
+ };
+ alertmanager = {
+ servers = [
+ {
+ name = "alertmanager";
+ uri = "https://127.0.0.1:9093";
+ }
+ ];
+ };
+ karma.name = "test-dashboard";
+ log.config = true;
+ log.requests = true;
+ log.timestamp = true;
+ };
+ };
+ };
+ };
+
+ testScript = ''
+ start_all()
+
+ with subtest("Wait for server to come up"):
+
+ server.wait_for_unit("alertmanager.service")
+ server.wait_for_unit("karma.service")
+
+ server.sleep(5) # wait for both services to settle
+
+ server.wait_for_open_port(9093)
+ server.wait_for_open_port(8081)
+
+ with subtest("Test alertmanager readiness"):
+ server.succeed("curl -s http://127.0.0.1:9093/-/ready")
+
+ # Karma only starts serving the dashboard once it has established connectivity to all alertmanagers in its config
+ # Therefore, this will fail if karma isn't able to reach alertmanager
+ server.succeed("curl -s http://127.0.0.1:8081")
+
+ server.shutdown()
+ '';
+})
diff --git a/pkgs/servers/monitoring/karma/default.nix b/pkgs/servers/monitoring/karma/default.nix
new file mode 100644
index 000000000000..999f3176815f
--- /dev/null
+++ b/pkgs/servers/monitoring/karma/default.nix
@@ -0,0 +1,86 @@
+{ pkgs
+, lib
+, stdenv
+, buildGoModule
+, callPackage
+, fetchFromGitHub
+, nixosTests
+, nodejs-18_x
+}:
+
+let
+ uiNodeDependencies = (import ./node-composition.nix {
+ inherit pkgs;
+ inherit (stdenv.hostPlatform) system;
+ # pin nodejs version
+ nodejs = nodejs-18_x;
+ }).nodeDependencies;
+in
+
+buildGoModule rec {
+ pname = "karma";
+ version = "0.108";
+
+ src = fetchFromGitHub {
+ owner = "prymitive";
+ repo = "karma";
+ rev = "v${version}";
+ hash = "sha256-Hw+6zBBbVw8uGkZQvmS3gB1opZdg8wQ5LydIAWhXUXY=";
+ };
+
+ vendorHash = "sha256-gW8q425ZgFL+uJ8UiSVEX7R890LR9jgxfbNIdhNJge4=";
+
+ nativeBuildInputs = [
+ nodejs-18_x
+ ];
+
+ postPatch = ''
+ # Since we're using node2nix packages, the NODE_INSTALL hook isn't needed in the makefile
+ sed -i \
+ -e 's/$(NODE_INSTALL)//g' ./ui/Makefile \
+ -e 's~NODE_PATH := $(shell npm bin)~NODE_PATH := ./node_modules~g' ./ui/Makefile \
+ -e 's~NODE_MODULES := $(shell dirname `npm bin`)~NODE_MODULES := ./~g' ./ui/Makefile
+ '';
+
+ buildPhase = ''
+ # node will fail without this
+ export HOME=$(mktemp -d)
+
+ # build requires a writable .cache directory, so we'll create a
+ # temporary writable node_modules dir and link every package to it
+
+ # simply linking the node_modules directory would increase the closure size for uiNodeDependencies to >700MB
+ cp -r ${uiNodeDependencies}/lib/node_modules ./ui/
+ chmod -R +w ./ui/node_modules
+ mkdir -p ./ui/node_modules/.bin
+
+ pushd ./ui/node_modules/.bin
+
+ for x in ${uiNodeDependencies}/lib/node_modules/.bin/*; do
+ ln -sfv ./$(readlink "$x") ./$(basename "$x")
+ done
+
+ popd
+
+ mkdir -p ./ui/node_modules/.cache
+
+ # build package
+ VERSION="v${version}" make -j$NIX_BUILD_CORES
+
+ # clean up
+ rm -rf ./ui/node_modules
+ '';
+
+ installPhase = ''
+ install -Dm 755 ./karma $out/bin/karma
+ '';
+
+ passthru.tests.karma = nixosTests.karma;
+
+ meta = with lib; {
+ description = "Alert dashboard for Prometheus Alertmanager";
+ homepage = "https://karma-dashboard.io/";
+ license = licenses.asl20;
+ maintainers = with maintainers; [ nukaduka ];
+ };
+}
diff --git a/pkgs/servers/monitoring/karma/generate-dependencies.sh b/pkgs/servers/monitoring/karma/generate-dependencies.sh
new file mode 100755
index 000000000000..e37e125b7b12
--- /dev/null
+++ b/pkgs/servers/monitoring/karma/generate-dependencies.sh
@@ -0,0 +1,30 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p nodePackages.node2nix nodejs-18_x gnused wget
+
+# Usage: ./generate-dependencies.sh
+# Use the same version format as in ./default.nix (without the 'v')
+
+set -eou pipefail
+
+version=$1
+
+
+echo "Karma version: $version"
+cd "$(dirname $(readlink -f $0))"
+
+wget -O ./package-lock.json https://raw.githubusercontent.com/prymitive/karma/v$version/ui/package-lock.json
+wget -O ./package.json https://raw.githubusercontent.com/prymitive/karma/v$version/ui/package.json
+
+sed -i -e 's/"name": "ui",/"name": "karma-ui",/g' ./package.json
+sed -i -e 's/"name": "ui",/"name": "karma-ui",/g' ./package-lock.json
+
+node2nix \
+ --nodejs-18 \
+ --development \
+ -l ./package-lock.json \
+ -i ./package.json \
+ -o ./node-packages.nix \
+ -c ./node-composition.nix \
+ -e ./node-env.nix
+
+rm package-lock.json
diff --git a/pkgs/servers/monitoring/karma/node-composition.nix b/pkgs/servers/monitoring/karma/node-composition.nix
new file mode 100644
index 000000000000..6bbc431049ee
--- /dev/null
+++ b/pkgs/servers/monitoring/karma/node-composition.nix
@@ -0,0 +1,17 @@
+# This file has been generated by node2nix 1.11.1. Do not edit!
+
+{pkgs ? import {
+ inherit system;
+ }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs-18_x"}:
+
+let
+ nodeEnv = import ./node-env.nix {
+ inherit (pkgs) stdenv lib python2 runCommand writeTextFile writeShellScript;
+ inherit pkgs nodejs;
+ libtool = if pkgs.stdenv.isDarwin then pkgs.darwin.cctools else null;
+ };
+in
+import ./node-packages.nix {
+ inherit (pkgs) fetchurl nix-gitignore stdenv lib fetchgit;
+ inherit nodeEnv;
+}
diff --git a/pkgs/servers/monitoring/karma/node-env.nix b/pkgs/servers/monitoring/karma/node-env.nix
new file mode 100644
index 000000000000..5dad9ec63d47
--- /dev/null
+++ b/pkgs/servers/monitoring/karma/node-env.nix
@@ -0,0 +1,686 @@
+# This file originates from node2nix
+
+{lib, stdenv, nodejs, python2, pkgs, libtool, runCommand, writeTextFile, writeShellScript}:
+
+let
+ # Workaround to cope with utillinux in Nixpkgs 20.09 and util-linux in Nixpkgs master
+ utillinux = if pkgs ? utillinux then pkgs.utillinux else pkgs.util-linux;
+
+ python = if nodejs ? python then nodejs.python else python2;
+
+ # Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise
+ tarWrapper = runCommand "tarWrapper" {} ''
+ mkdir -p $out/bin
+
+ cat > $out/bin/tar <> $out/nix-support/hydra-build-products
+ '';
+ };
+
+ # Common shell logic
+ installPackage = writeShellScript "install-package" ''
+ installPackage() {
+ local packageName=$1 src=$2
+
+ local strippedName
+
+ local DIR=$PWD
+ cd $TMPDIR
+
+ unpackFile $src
+
+ # Make the base dir in which the target dependency resides first
+ mkdir -p "$(dirname "$DIR/$packageName")"
+
+ if [ -f "$src" ]
+ then
+ # Figure out what directory has been unpacked
+ packageDir="$(find . -maxdepth 1 -type d | tail -1)"
+
+ # Restore write permissions to make building work
+ find "$packageDir" -type d -exec chmod u+x {} \;
+ chmod -R u+w "$packageDir"
+
+ # Move the extracted tarball into the output folder
+ mv "$packageDir" "$DIR/$packageName"
+ elif [ -d "$src" ]
+ then
+ # Get a stripped name (without hash) of the source directory.
+ # On old nixpkgs it's already set internally.
+ if [ -z "$strippedName" ]
+ then
+ strippedName="$(stripHash $src)"
+ fi
+
+ # Restore write permissions to make building work
+ chmod -R u+w "$strippedName"
+
+ # Move the extracted directory into the output folder
+ mv "$strippedName" "$DIR/$packageName"
+ fi
+
+ # Change to the package directory to install dependencies
+ cd "$DIR/$packageName"
+ }
+ '';
+
+ # Bundle the dependencies of the package
+ #
+ # Only include dependencies if they don't exist. They may also be bundled in the package.
+ includeDependencies = {dependencies}:
+ lib.optionalString (dependencies != []) (
+ ''
+ mkdir -p node_modules
+ cd node_modules
+ ''
+ + (lib.concatMapStrings (dependency:
+ ''
+ if [ ! -e "${dependency.packageName}" ]; then
+ ${composePackage dependency}
+ fi
+ ''
+ ) dependencies)
+ + ''
+ cd ..
+ ''
+ );
+
+ # Recursively composes the dependencies of a package
+ composePackage = { name, packageName, src, dependencies ? [], ... }@args:
+ builtins.addErrorContext "while evaluating node package '${packageName}'" ''
+ installPackage "${packageName}" "${src}"
+ ${includeDependencies { inherit dependencies; }}
+ cd ..
+ ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
+ '';
+
+ pinpointDependencies = {dependencies, production}:
+ let
+ pinpointDependenciesFromPackageJSON = writeTextFile {
+ name = "pinpointDependencies.js";
+ text = ''
+ var fs = require('fs');
+ var path = require('path');
+
+ function resolveDependencyVersion(location, name) {
+ if(location == process.env['NIX_STORE']) {
+ return null;
+ } else {
+ var dependencyPackageJSON = path.join(location, "node_modules", name, "package.json");
+
+ if(fs.existsSync(dependencyPackageJSON)) {
+ var dependencyPackageObj = JSON.parse(fs.readFileSync(dependencyPackageJSON));
+
+ if(dependencyPackageObj.name == name) {
+ return dependencyPackageObj.version;
+ }
+ } else {
+ return resolveDependencyVersion(path.resolve(location, ".."), name);
+ }
+ }
+ }
+
+ function replaceDependencies(dependencies) {
+ if(typeof dependencies == "object" && dependencies !== null) {
+ for(var dependency in dependencies) {
+ var resolvedVersion = resolveDependencyVersion(process.cwd(), dependency);
+
+ if(resolvedVersion === null) {
+ process.stderr.write("WARNING: cannot pinpoint dependency: "+dependency+", context: "+process.cwd()+"\n");
+ } else {
+ dependencies[dependency] = resolvedVersion;
+ }
+ }
+ }
+ }
+
+ /* Read the package.json configuration */
+ var packageObj = JSON.parse(fs.readFileSync('./package.json'));
+
+ /* Pinpoint all dependencies */
+ replaceDependencies(packageObj.dependencies);
+ if(process.argv[2] == "development") {
+ replaceDependencies(packageObj.devDependencies);
+ }
+ else {
+ packageObj.devDependencies = {};
+ }
+ replaceDependencies(packageObj.optionalDependencies);
+ replaceDependencies(packageObj.peerDependencies);
+
+ /* Write the fixed package.json file */
+ fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2));
+ '';
+ };
+ in
+ ''
+ node ${pinpointDependenciesFromPackageJSON} ${if production then "production" else "development"}
+
+ ${lib.optionalString (dependencies != [])
+ ''
+ if [ -d node_modules ]
+ then
+ cd node_modules
+ ${lib.concatMapStrings (dependency: pinpointDependenciesOfPackage dependency) dependencies}
+ cd ..
+ fi
+ ''}
+ '';
+
+ # Recursively traverses all dependencies of a package and pinpoints all
+ # dependencies in the package.json file to the versions that are actually
+ # being used.
+
+ pinpointDependenciesOfPackage = { packageName, dependencies ? [], production ? true, ... }@args:
+ ''
+ if [ -d "${packageName}" ]
+ then
+ cd "${packageName}"
+ ${pinpointDependencies { inherit dependencies production; }}
+ cd ..
+ ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
+ fi
+ '';
+
+ # Extract the Node.js source code which is used to compile packages with
+ # native bindings
+ nodeSources = runCommand "node-sources" {} ''
+ tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
+ mv node-* $out
+ '';
+
+ # Script that adds _integrity fields to all package.json files to prevent NPM from consulting the cache (that is empty)
+ addIntegrityFieldsScript = writeTextFile {
+ name = "addintegrityfields.js";
+ text = ''
+ var fs = require('fs');
+ var path = require('path');
+
+ function augmentDependencies(baseDir, dependencies) {
+ for(var dependencyName in dependencies) {
+ var dependency = dependencies[dependencyName];
+
+ // Open package.json and augment metadata fields
+ var packageJSONDir = path.join(baseDir, "node_modules", dependencyName);
+ var packageJSONPath = path.join(packageJSONDir, "package.json");
+
+ if(fs.existsSync(packageJSONPath)) { // Only augment packages that exist. Sometimes we may have production installs in which development dependencies can be ignored
+ console.log("Adding metadata fields to: "+packageJSONPath);
+ var packageObj = JSON.parse(fs.readFileSync(packageJSONPath));
+
+ if(dependency.integrity) {
+ packageObj["_integrity"] = dependency.integrity;
+ } else {
+ packageObj["_integrity"] = "sha1-000000000000000000000000000="; // When no _integrity string has been provided (e.g. by Git dependencies), add a dummy one. It does not seem to harm and it bypasses downloads.
+ }
+
+ if(dependency.resolved) {
+ packageObj["_resolved"] = dependency.resolved; // Adopt the resolved property if one has been provided
+ } else {
+ packageObj["_resolved"] = dependency.version; // Set the resolved version to the version identifier. This prevents NPM from cloning Git repositories.
+ }
+
+ if(dependency.from !== undefined) { // Adopt from property if one has been provided
+ packageObj["_from"] = dependency.from;
+ }
+
+ fs.writeFileSync(packageJSONPath, JSON.stringify(packageObj, null, 2));
+ }
+
+ // Augment transitive dependencies
+ if(dependency.dependencies !== undefined) {
+ augmentDependencies(packageJSONDir, dependency.dependencies);
+ }
+ }
+ }
+
+ if(fs.existsSync("./package-lock.json")) {
+ var packageLock = JSON.parse(fs.readFileSync("./package-lock.json"));
+
+ if(![1, 2].includes(packageLock.lockfileVersion)) {
+ process.stderr.write("Sorry, I only understand lock file versions 1 and 2!\n");
+ process.exit(1);
+ }
+
+ if(packageLock.dependencies !== undefined) {
+ augmentDependencies(".", packageLock.dependencies);
+ }
+ }
+ '';
+ };
+
+ # Reconstructs a package-lock file from the node_modules/ folder structure and package.json files with dummy sha1 hashes
+ reconstructPackageLock = writeTextFile {
+ name = "reconstructpackagelock.js";
+ text = ''
+ var fs = require('fs');
+ var path = require('path');
+
+ var packageObj = JSON.parse(fs.readFileSync("package.json"));
+
+ var lockObj = {
+ name: packageObj.name,
+ version: packageObj.version,
+ lockfileVersion: 2,
+ requires: true,
+ packages: {
+ "": {
+ name: packageObj.name,
+ version: packageObj.version,
+ license: packageObj.license,
+ bin: packageObj.bin,
+ dependencies: packageObj.dependencies,
+ engines: packageObj.engines,
+ optionalDependencies: packageObj.optionalDependencies
+ }
+ },
+ dependencies: {}
+ };
+
+ function augmentPackageJSON(filePath, packages, dependencies) {
+ var packageJSON = path.join(filePath, "package.json");
+ if(fs.existsSync(packageJSON)) {
+ var packageObj = JSON.parse(fs.readFileSync(packageJSON));
+ packages[filePath] = {
+ version: packageObj.version,
+ integrity: "sha1-000000000000000000000000000=",
+ dependencies: packageObj.dependencies,
+ engines: packageObj.engines,
+ optionalDependencies: packageObj.optionalDependencies
+ };
+ dependencies[packageObj.name] = {
+ version: packageObj.version,
+ integrity: "sha1-000000000000000000000000000=",
+ dependencies: {}
+ };
+ processDependencies(path.join(filePath, "node_modules"), packages, dependencies[packageObj.name].dependencies);
+ }
+ }
+
+ function processDependencies(dir, packages, dependencies) {
+ if(fs.existsSync(dir)) {
+ var files = fs.readdirSync(dir);
+
+ files.forEach(function(entry) {
+ var filePath = path.join(dir, entry);
+ var stats = fs.statSync(filePath);
+
+ if(stats.isDirectory()) {
+ if(entry.substr(0, 1) == "@") {
+ // When we encounter a namespace folder, augment all packages belonging to the scope
+ var pkgFiles = fs.readdirSync(filePath);
+
+ pkgFiles.forEach(function(entry) {
+ if(stats.isDirectory()) {
+ var pkgFilePath = path.join(filePath, entry);
+ augmentPackageJSON(pkgFilePath, packages, dependencies);
+ }
+ });
+ } else {
+ augmentPackageJSON(filePath, packages, dependencies);
+ }
+ }
+ });
+ }
+ }
+
+ processDependencies("node_modules", lockObj.packages, lockObj.dependencies);
+
+ fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2));
+ '';
+ };
+
+ # Script that links bins defined in package.json to the node_modules bin directory
+ # NPM does not do this for top-level packages itself anymore as of v7
+ linkBinsScript = writeTextFile {
+ name = "linkbins.js";
+ text = ''
+ var fs = require('fs');
+ var path = require('path');
+
+ var packageObj = JSON.parse(fs.readFileSync("package.json"));
+
+ var nodeModules = Array(packageObj.name.split("/").length).fill("..").join(path.sep);
+
+ if(packageObj.bin !== undefined) {
+ fs.mkdirSync(path.join(nodeModules, ".bin"))
+
+ if(typeof packageObj.bin == "object") {
+ Object.keys(packageObj.bin).forEach(function(exe) {
+ if(fs.existsSync(packageObj.bin[exe])) {
+ console.log("linking bin '" + exe + "'");
+ fs.symlinkSync(
+ path.join("..", packageObj.name, packageObj.bin[exe]),
+ path.join(nodeModules, ".bin", exe)
+ );
+ }
+ else {
+ console.log("skipping non-existent bin '" + exe + "'");
+ }
+ })
+ }
+ else {
+ if(fs.existsSync(packageObj.bin)) {
+ console.log("linking bin '" + packageObj.bin + "'");
+ fs.symlinkSync(
+ path.join("..", packageObj.name, packageObj.bin),
+ path.join(nodeModules, ".bin", packageObj.name.split("/").pop())
+ );
+ }
+ else {
+ console.log("skipping non-existent bin '" + packageObj.bin + "'");
+ }
+ }
+ }
+ else if(packageObj.directories !== undefined && packageObj.directories.bin !== undefined) {
+ fs.mkdirSync(path.join(nodeModules, ".bin"))
+
+ fs.readdirSync(packageObj.directories.bin).forEach(function(exe) {
+ if(fs.existsSync(path.join(packageObj.directories.bin, exe))) {
+ console.log("linking bin '" + exe + "'");
+ fs.symlinkSync(
+ path.join("..", packageObj.name, packageObj.directories.bin, exe),
+ path.join(nodeModules, ".bin", exe)
+ );
+ }
+ else {
+ console.log("skipping non-existent bin '" + exe + "'");
+ }
+ })
+ }
+ '';
+ };
+
+ prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}:
+ let
+ forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com";
+ in
+ ''
+ # Pinpoint the versions of all dependencies to the ones that are actually being used
+ echo "pinpointing versions of dependencies..."
+ source $pinpointDependenciesScriptPath
+
+ # Patch the shebangs of the bundled modules to prevent them from
+ # calling executables outside the Nix store as much as possible
+ patchShebangs .
+
+ # Deploy the Node.js package by running npm install. Since the
+ # dependencies have been provided already by ourselves, it should not
+ # attempt to install them again, which is good, because we want to make
+ # it Nix's responsibility. If it needs to install any dependencies
+ # anyway (e.g. because the dependency parameters are
+ # incomplete/incorrect), it fails.
+ #
+ # The other responsibilities of NPM are kept -- version checks, build
+ # steps, postprocessing etc.
+
+ export HOME=$TMPDIR
+ cd "${packageName}"
+ runHook preRebuild
+
+ ${lib.optionalString bypassCache ''
+ ${lib.optionalString reconstructLock ''
+ if [ -f package-lock.json ]
+ then
+ echo "WARNING: Reconstruct lock option enabled, but a lock file already exists!"
+ echo "This will most likely result in version mismatches! We will remove the lock file and regenerate it!"
+ rm package-lock.json
+ else
+ echo "No package-lock.json file found, reconstructing..."
+ fi
+
+ node ${reconstructPackageLock}
+ ''}
+
+ node ${addIntegrityFieldsScript}
+ ''}
+
+ npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} rebuild
+
+ runHook postRebuild
+
+ if [ "''${dontNpmInstall-}" != "1" ]
+ then
+ # NPM tries to download packages even when they already exist if npm-shrinkwrap is used.
+ rm -f npm-shrinkwrap.json
+
+ npm ${forceOfflineFlag} --nodedir=${nodeSources} --no-bin-links --ignore-scripts ${npmFlags} ${lib.optionalString production "--production"} install
+ fi
+
+ # Link executables defined in package.json
+ node ${linkBinsScript}
+ '';
+
+ # Builds and composes an NPM package including all its dependencies
+ buildNodePackage =
+ { name
+ , packageName
+ , version ? null
+ , dependencies ? []
+ , buildInputs ? []
+ , production ? true
+ , npmFlags ? ""
+ , dontNpmInstall ? false
+ , bypassCache ? false
+ , reconstructLock ? false
+ , preRebuild ? ""
+ , dontStrip ? true
+ , unpackPhase ? "true"
+ , buildPhase ? "true"
+ , meta ? {}
+ , ... }@args:
+
+ let
+ extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "preRebuild" "unpackPhase" "buildPhase" "meta" ];
+ in
+ stdenv.mkDerivation ({
+ name = "${name}${if version == null then "" else "-${version}"}";
+ buildInputs = [ tarWrapper python nodejs ]
+ ++ lib.optional (stdenv.isLinux) utillinux
+ ++ lib.optional (stdenv.isDarwin) libtool
+ ++ buildInputs;
+
+ inherit nodejs;
+
+ inherit dontStrip; # Stripping may fail a build for some package deployments
+ inherit dontNpmInstall preRebuild unpackPhase buildPhase;
+
+ compositionScript = composePackage args;
+ pinpointDependenciesScript = pinpointDependenciesOfPackage args;
+
+ passAsFile = [ "compositionScript" "pinpointDependenciesScript" ];
+
+ installPhase = ''
+ source ${installPackage}
+
+ # Create and enter a root node_modules/ folder
+ mkdir -p $out/lib/node_modules
+ cd $out/lib/node_modules
+
+ # Compose the package and all its dependencies
+ source $compositionScriptPath
+
+ ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
+
+ # Create symlink to the deployed executable folder, if applicable
+ if [ -d "$out/lib/node_modules/.bin" ]
+ then
+ ln -s $out/lib/node_modules/.bin $out/bin
+
+ # Patch the shebang lines of all the executables
+ ls $out/bin/* | while read i
+ do
+ file="$(readlink -f "$i")"
+ chmod u+rwx "$file"
+ patchShebangs "$file"
+ done
+ fi
+
+ # Create symlinks to the deployed manual page folders, if applicable
+ if [ -d "$out/lib/node_modules/${packageName}/man" ]
+ then
+ mkdir -p $out/share
+ for dir in "$out/lib/node_modules/${packageName}/man/"*
+ do
+ mkdir -p $out/share/man/$(basename "$dir")
+ for page in "$dir"/*
+ do
+ ln -s $page $out/share/man/$(basename "$dir")
+ done
+ done
+ fi
+
+ # Run post install hook, if provided
+ runHook postInstall
+ '';
+
+ meta = {
+ # default to Node.js' platforms
+ platforms = nodejs.meta.platforms;
+ } // meta;
+ } // extraArgs);
+
+ # Builds a node environment (a node_modules folder and a set of binaries)
+ buildNodeDependencies =
+ { name
+ , packageName
+ , version ? null
+ , src
+ , dependencies ? []
+ , buildInputs ? []
+ , production ? true
+ , npmFlags ? ""
+ , dontNpmInstall ? false
+ , bypassCache ? false
+ , reconstructLock ? false
+ , dontStrip ? true
+ , unpackPhase ? "true"
+ , buildPhase ? "true"
+ , ... }@args:
+
+ let
+ extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ];
+ in
+ stdenv.mkDerivation ({
+ name = "node-dependencies-${name}${if version == null then "" else "-${version}"}";
+
+ buildInputs = [ tarWrapper python nodejs ]
+ ++ lib.optional (stdenv.isLinux) utillinux
+ ++ lib.optional (stdenv.isDarwin) libtool
+ ++ buildInputs;
+
+ inherit dontStrip; # Stripping may fail a build for some package deployments
+ inherit dontNpmInstall unpackPhase buildPhase;
+
+ includeScript = includeDependencies { inherit dependencies; };
+ pinpointDependenciesScript = pinpointDependenciesOfPackage args;
+
+ passAsFile = [ "includeScript" "pinpointDependenciesScript" ];
+
+ installPhase = ''
+ source ${installPackage}
+
+ mkdir -p $out/${packageName}
+ cd $out/${packageName}
+
+ source $includeScriptPath
+
+ # Create fake package.json to make the npm commands work properly
+ cp ${src}/package.json .
+ chmod 644 package.json
+ ${lib.optionalString bypassCache ''
+ if [ -f ${src}/package-lock.json ]
+ then
+ cp ${src}/package-lock.json .
+ chmod 644 package-lock.json
+ fi
+ ''}
+
+ # Go to the parent folder to make sure that all packages are pinpointed
+ cd ..
+ ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
+
+ ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
+
+ # Expose the executables that were installed
+ cd ..
+ ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
+
+ mv ${packageName} lib
+ ln -s $out/lib/node_modules/.bin $out/bin
+ '';
+ } // extraArgs);
+
+ # Builds a development shell
+ buildNodeShell =
+ { name
+ , packageName
+ , version ? null
+ , src
+ , dependencies ? []
+ , buildInputs ? []
+ , production ? true
+ , npmFlags ? ""
+ , dontNpmInstall ? false
+ , bypassCache ? false
+ , reconstructLock ? false
+ , dontStrip ? true
+ , unpackPhase ? "true"
+ , buildPhase ? "true"
+ , ... }@args:
+
+ let
+ nodeDependencies = buildNodeDependencies args;
+ extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "unpackPhase" "buildPhase" ];
+ in
+ stdenv.mkDerivation ({
+ name = "node-shell-${name}${if version == null then "" else "-${version}"}";
+
+ buildInputs = [ python nodejs ] ++ lib.optional (stdenv.isLinux) utillinux ++ buildInputs;
+ buildCommand = ''
+ mkdir -p $out/bin
+ cat > $out/bin/shell <=8.3 <9.0",
+ "node": "18.8.0"
+ },
+ "dependencies": {
+ "@floating-ui/react-dom": "1.0.0",
+ "@fortawesome/fontawesome-svg-core": "6.2.0",
+ "@fortawesome/free-regular-svg-icons": "6.2.0",
+ "@fortawesome/free-solid-svg-icons": "6.2.0",
+ "@fortawesome/react-fontawesome": "0.2.0",
+ "@juggle/resize-observer": "3.4.0",
+ "body-scroll-lock": "3.1.5",
+ "bootstrap": "5.2.0",
+ "bootswatch": "5.2.0",
+ "bricks.js": "1.8.0",
+ "copy-to-clipboard": "3.3.2",
+ "csshake": "1.7.0",
+ "date-fns": "2.29.2",
+ "downshift": "6.1.9",
+ "favico.js": "0.3.10",
+ "fontfaceobserver": "2.3.0",
+ "intersection-observer": "0.12.2",
+ "lodash.debounce": "4.0.8",
+ "lodash.merge": "4.6.2",
+ "lodash.throttle": "4.1.1",
+ "lodash.uniqueid": "4.0.1",
+ "mobx": "6.6.1",
+ "mobx-react-lite": "3.4.0",
+ "mobx-stored": "1.1.0",
+ "promise-retry": "2.0.1",
+ "qs": "6.11.0",
+ "react": "17.0.2",
+ "react-app-polyfill": "3.0.0",
+ "react-cool-dimensions": "2.0.7",
+ "react-day-picker": "8.2.0",
+ "react-dom": "17.0.2",
+ "react-hotkeys-hook": "3.4.7",
+ "react-idle-timer": "4.6.4",
+ "react-intersection-observer": "9.4.0",
+ "react-json-pretty": "2.2.0",
+ "react-linkify": "0.2.2",
+ "react-media-hook": "0.4.9",
+ "react-range": "1.8.14",
+ "react-select": "5.4.0",
+ "react-transition-group": "4.4.5",
+ "typeface-open-sans": "1.1.13"
+ },
+ "devDependencies": {
+ "@percy/cli": "1.10.1",
+ "@percy/storybook": "4.3.3",
+ "@storybook/builder-webpack5": "6.5.10",
+ "@storybook/manager-webpack5": "6.5.10",
+ "@storybook/preset-create-react-app": "4.1.2",
+ "@storybook/react": "6.5.10",
+ "@testing-library/jest-dom": "5.16.5",
+ "@testing-library/react": "12.1.5",
+ "@testing-library/react-hooks": "8.0.1",
+ "@testing-library/user-event": "14.4.3",
+ "@types/body-scroll-lock": "3.1.0",
+ "@types/bricks.js": "1.8.2",
+ "@types/enzyme": "3.10.12",
+ "@types/fontfaceobserver": "2.1.0",
+ "@types/jest": "29.0.0",
+ "@types/lodash.debounce": "4.0.7",
+ "@types/lodash.merge": "4.6.7",
+ "@types/lodash.throttle": "4.1.7",
+ "@types/lodash.uniqueid": "4.0.7",
+ "@types/node": "16.11.56",
+ "@types/promise-retry": "1.1.3",
+ "@types/qs": "6.9.7",
+ "@types/react": "17.0.49",
+ "@types/react-dom": "17.0.17",
+ "@wojtekmaj/enzyme-adapter-react-17": "0.6.7",
+ "diffable-html": "5.0.0",
+ "enzyme": "3.11.0",
+ "eslint-config-prettier": "8.5.0",
+ "eslint-config-react-app": "7.0.1",
+ "eslint-plugin-jest": "27.0.1",
+ "eslint-plugin-prettier": "4.2.1",
+ "fetch-mock": "9.11.0",
+ "prettier": "2.7.1",
+ "purgecss": "4.1.3",
+ "react-scripts": "5.0.1",
+ "sass": "1.54.8",
+ "timemachine": "0.3.2",
+ "typescript": "4.8.2"
+ },
+ "overrides": {
+ "react-scripts@5.0.1": {
+ "autoprefixer": "10.4.5"
+ }
+ },
+ "scripts": {
+ "start": "REACT_APP_BACKEND_URI=http://localhost:8080 react-scripts start",
+ "build": "react-scripts build",
+ "postbuild": "make backup-css && purgecss -c purgecss.config.js -o build/static/css",
+ "test": "react-scripts test",
+ "storybook": "start-storybook",
+ "snapshot": "build-storybook --quiet && percy storybook --verbose ./storybook-static",
+ "analyze": "npx source-map-explorer 'build/static/js/*.js'"
+ },
+ "jest": {
+ "collectCoverageFrom": [
+ "src/**/*.{js,ts,tsx}",
+ "!src/**/*.stories.{js,ts,tsx}",
+ "!src/__fixtures__/Stories.{js,ts,tsx}",
+ "!src/react-app-env.d.ts",
+ "!src/Models/*.ts"
+ ]
+ },
+ "browserslist": [
+ ">= 1%",
+ "last 1 major version",
+ "not dead",
+ "Chrome >= 49",
+ "Firefox >= 52",
+ "Edge >= 12",
+ "iOS >= 9",
+ "Safari >= 10",
+ "Android >= 4.4"
+ ],
+ "percy": {
+ "version": 2,
+ "snapshot": {
+ "widths": [
+ 700,
+ 1280
+ ],
+ "min-height": 200,
+ "enable-javascript": true
+ }
+ }
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 2c3ac150b8bd..de1e15527c9d 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -4305,6 +4305,8 @@ with pkgs;
kapacitor = callPackage ../servers/monitoring/kapacitor { };
+ karma = callPackage ../servers/monitoring/karma { };
+
kaldi = callPackage ../tools/audio/kaldi { };
kalker = callPackage ../tools/misc/kalker { };