mirror of https://github.com/micromata/borgbackup-butler.git

Kai Reinhard
09.43.2018 1a21e0d3baca3870611a1fe712027a559d9a4b93
Web server started.
100 files added
2 files modified
5068 ■■■■■ changed files
.gitignore 1 ●●●● patch | view | raw | blame | history
borgbutler-server/build.gradle 93 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/Languages.java 20 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java 113 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java 85 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java 69 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfigurationHandler.java 112 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/Version.java 107 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/jetty/JettyServer.java 177 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/Log4jMemoryAppender.java 136 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogFilter.java 71 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java 42 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java 121 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java 87 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java 41 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/LoggingRest.java 70 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/RestUtils.java 58 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java 88 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java 55 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserData.java 59 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java 53 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserManager.java 24 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserUtils.java 53 ●●●●● patch | view | raw | blame | history
borgbutler-server/src/main/resources/log4j.properties 19 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/build.gradle 8 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/package.json 40 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/public/browserconfig.xml 2 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/public/css/bootstrap.min.css 7 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/public/favicon.ico patch | view | raw | blame | history
borgbutler-webapp/public/favicon/android-icon-144x144.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/android-icon-192x192.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/android-icon-36x36.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/android-icon-48x48.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/android-icon-72x72.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/android-icon-96x96.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-114x114.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-120x120.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-144x144.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-152x152.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-180x180.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-57x57.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-60x60.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-72x72.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-76x76.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon-precomposed.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/apple-icon.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/favicon-16x16.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/favicon-32x32.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/favicon-96x96.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/ms-icon-144x144.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/ms-icon-150x150.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/ms-icon-310x310.png patch | view | raw | blame | history
borgbutler-webapp/public/favicon/ms-icon-70x70.png patch | view | raw | blame | history
borgbutler-webapp/public/images/merlin-icon.png patch | view | raw | blame | history
borgbutler-webapp/public/index.html 32 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/public/manifest.json 46 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/actions/index.js 6 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/actions/log.js 54 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/actions/types.js 8 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/actions/version.js 38 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/BootstrapComponents.jsx 23 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/ErrorAlert.jsx 20 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/ErrorAlertGenericRestFailure.jsx 13 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/IconComponents.jsx 127 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/LinkFile.jsx 41 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/Loading.jsx 8 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/Menu.jsx 89 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/OpenLocalFile.jsx 47 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/forms/EditableTextField.css 33 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/forms/EditableTextField.jsx 136 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/forms/FormButton.jsx 55 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/forms/FormCheckbox.jsx 62 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/forms/FormComponents.jsx 298 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/forms/FormSelect.jsx 71 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/loading/LoadingOverlay.jsx 28 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/loading/LoadingOverlay.module.css 18 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/loading/failed/Overlay.jsx 15 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/general/translation/I18n.jsx 20 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/Start.jsx 20 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/config/ConfigurationAccountTab.jsx 135 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx 120 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx 215 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/config/UpdatePage.jsx 126 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/develop/RestServices.jsx 146 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/footer/Footer.jsx 25 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/footer/style.css 12 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/logging/LogEmbeddedPanel.jsx 106 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/logging/LogEntry.jsx 24 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/logging/LogFilters.jsx 91 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/logging/LogPage.jsx 89 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/logging/LogTable.jsx 72 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/components/views/logging/LogViewer.css 12 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/containers/WebApp.jsx 77 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/containers/WebApp.test.js 9 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/css/my-style.css 406 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/index.js 47 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/reducers/index.js 12 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/reducers/log.js 52 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/reducers/version.js 44 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/utilities/global.js 49 ●●●●● patch | view | raw | blame | history
borgbutler-webapp/src/utilities/i18n.js 73 ●●●●● patch | view | raw | blame | history
settings.gradle 7 ●●●●● patch | view | raw | blame | history
.gitignore
@@ -37,3 +37,4 @@
*.iws
borgbutler-core/build
borgbutler-core/out
borgbutler-server/out
borgbutler-server/build.gradle
New file
@@ -0,0 +1,93 @@
description = 'borgbutler-server'
buildscript {
    repositories {
        mavenCentral()
    }
}
dependencies {
    compile project(':borgbutler-core')
    // https://mvnrepository.com/artifact/org.apache.commons/commons-text
    compile group: 'org.apache.commons', name: 'commons-text', version: '1.6'
    compile group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.4.12.v20180830'
    compile group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '9.4.12.v20180830'
    compile group: 'org.eclipse.jetty', name: 'jetty-servlets', version: '9.4.12.v20180830'
    compile group: 'org.glassfish.jaxb', name: 'jaxb-core', version: '2.3.0.1'
    compile group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '2.3.1'
    compile group: 'org.glassfish.jersey.containers', name: 'jersey-container-servlet', version: '2.27'
    compile group: 'org.glassfish.jersey.media', name: 'jersey-media-multipart', version: '2.27'
    compile group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version: '2.27'
    compile group: 'org.glassfish.jersey.inject', name: 'jersey-hk2', version: '2.27'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.6'
    compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
    compile group: 'javax.xml.ws', name: 'jaxws-api', version: '2.3.1'
    compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25'
    // https://mvnrepository.com/artifact/commons-cli/commons-cli
    compile group: 'commons-cli', name: 'commons-cli', version: '1.4'
    testCompile group: 'org.mockito', name: 'mockito-core', version: '2.21.0'
}
apply plugin: 'application'
mainClassName = "de.micromata.borgbutler.server.Main"
run() {
    doFirst {
        jvmArgs = [
                "-DapplicationHome=${rootDir}"
        ]
    }
}
run.dependsOn ':borgbutler-webapp:npmBuild'
// run.dependsOn ':borgbutler-docs:buildWebDoc'
apply plugin: 'distribution'
task createVersionProperties(dependsOn: processResources) {
    doLast {
        new File("$buildDir/resources/main/version.properties").withWriter { w ->
            Properties p = new Properties()
            p['version'] = project.version.toString()
            p['name'] = project.name
            p['build.date.millis'] = '' + System.currentTimeMillis()
            p.store w, null
        }
    }
}
classes {
    dependsOn createVersionProperties
}
// Ugly work arround for getting the applications home dir:
applicationDefaultJvmArgs = ["-DapplicationHome=MY_APPLICATION_HOME"]
startScripts {
    doLast {
        unixScript.text = unixScript.text.replace('MY_APPLICATION_HOME', '\$APP_HOME')
        windowsScript.text = windowsScript.text.replace('MY_APPLICATION_HOME', '%~dp0..')
    }
}
// Builds the distribution
distributions {
    main {
        contents {
            // Prepared by nbmBuild:
            from ("${project(':borgbutler-webapp').projectDir}/build") {
                into 'web'
            }
            // Containing test templates and other stuff:
            from ("${rootProject.projectDir}/examples") {
                into 'examples'
            }
        }
    }
}
distZip.dependsOn ':borgbutler-webapp:npmBuild'
//distZip.dependsOn ':borgbutler-docs:buildWebDoc'
task(dist).dependsOn distZip
borgbutler-server/src/main/java/de/micromata/borgbutler/server/Languages.java
New file
@@ -0,0 +1,20 @@
package de.micromata.borgbutler.server;
import org.apache.commons.lang3.StringUtils;
import java.util.Locale;
public class Languages {
    public static Locale asLocale(String language) {
        return asLocale(language, false);
    }
    public static Locale asLocale(String language, boolean rootAsDefault) {
        Locale locale = StringUtils.isNotBlank(language) ? Locale.forLanguageTag(language) : null;
        return (locale != null || !rootAsDefault) ? locale : Locale.ROOT;
    }
    public static String asString(Locale locale) {
        return locale != null ? locale.getLanguage() : null;
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java
New file
@@ -0,0 +1,113 @@
package de.micromata.borgbutler.server;
import de.micromata.borgbutler.server.jetty.JettyServer;
import de.micromata.borgbutler.server.user.SingleUserManager;
import de.micromata.borgbutler.server.user.UserManager;
import org.apache.commons.cli.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
    private static Logger log = LoggerFactory.getLogger(Main.class);
    private static final Main main = new Main();
    private JettyServer server;
    private boolean shutdownInProgress;
    private Main() {
    }
    public static void main(String[] args) {
        main._start(args);
    }
    public static JettyServer startUp(String... restPackageNames) {
        return main._startUp(restPackageNames);
    }
    public static void shutdown() {
        main._shutdown();
    }
    private void _start(String[] args) {
        // create Options object
        Options options = new Options();
        options.addOption("p", "port", true, "The default port for the web server.");
        options.addOption("q", "quiet", false, "Don't open browser automatically.");
        options.addOption("h", "help", false, "Print this help screen.");
        CommandLineParser parser = new DefaultParser();
        try {
            // parse the command line arguments
            CommandLine line = parser.parse(options, args);
            if (line.hasOption('h')) {
                printHelp(options);
                return;
            }
            if (line.hasOption('p')) {
                // initialise the member variable
                String portString = line.getOptionValue("p");
                try {
                    int port = Integer.parseInt(portString);
                    if (port < 1 || port > 65535) {
                        System.err.println("Port outside range.");
                        return;
                    }
                    ServerConfigurationHandler.getDefaultConfiguration().setPort(port);
                } catch (NumberFormatException ex) {
                    printHelp(options);
                    return;
                }
            }
            RunningMode.setServerType(RunningMode.ServerType.SERVER);
            RunningMode.logMode();
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    main._shutdown();
                }
            });
            JettyServer server = startUp();
            if (!line.hasOption('q')) {
                try {
                    java.awt.Desktop.getDesktop().browse(java.net.URI.create(server.getUrl()));
                } catch (Exception ex) {
                    log.info("Can't open web browser: " + ex.getMessage());
                }
            }
        } catch (ParseException ex) {
            // oops, something went wrong
            System.err.println("Parsing failed.  Reason: " + ex.getMessage());
            printHelp(options);
        }
    }
    private JettyServer _startUp(String... restPackageNames) {
        server = new JettyServer();
        server.start(restPackageNames);
        UserManager.setUserManager(new SingleUserManager());
        return server;
    }
    private void _shutdown() {
        synchronized (this) {
            if (shutdownInProgress == true) {
                // Another thread already called this method. There is nothing further to do.
                return;
            }
            shutdownInProgress = true;
        }
        log.info("Shutting down BorgButler web server...");
        server.stop();
    }
    private static void printHelp(Options options) {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("borgbutler-server", options);
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java
New file
@@ -0,0 +1,85 @@
package de.micromata.borgbutler.server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.TimeZone;
public class RunningMode {
    private static Logger log = LoggerFactory.getLogger(RunningMode.class);
    private static OSType osType;
    public enum Mode {PRODUCTION, DEVELOPMENT}
    public enum ServerType {DESKTOP, SERVER}
    public enum UserManagement {SINGLE}
    public enum OSType {MAC_OS, WINDOWS, LINUX, OTHER}
    private static boolean running;
    private static File baseDir;
    private static Boolean development;
    private static ServerType serverType;
    private static UserManagement userManagement = UserManagement.SINGLE;
    public static Mode getMode() {
        return isDevelopmentMode() ? Mode.DEVELOPMENT : Mode.PRODUCTION;
    }
    public static boolean isDevelopmentMode() {
        if (development == null) {
            development = new File(ServerConfiguration.getApplicationHome(), "merlin-core").exists();
            if (development) {
                log.warn("*** Starting Merlin server in development mode. This mode shouldn't be used in production environments. ***");
            }
        }
        return development;
    }
    public static OSType getOSType() {
        if (osType == null) {
            String osTypeString = System.getProperty("os.name");
            if (osTypeString == null) {
                osType = OSType.OTHER;
            } else if (osTypeString.toLowerCase().contains("mac")) {
                osType = OSType.MAC_OS;
            } else if (osTypeString.toLowerCase().contains("win")) {
                osType = OSType.WINDOWS;
            } else if (osTypeString.toLowerCase().contains("linux")) {
                osType = OSType.LINUX;
            } else {
                osType = OSType.OTHER;
            }
        }
        return osType;
    }
    public static ServerType getServerType() {
        return serverType;
    }
    public static void setServerType(ServerType serverType) {
        if (RunningMode.serverType != null && serverType != RunningMode.serverType) {
            throw new IllegalArgumentException("Can't set server-type twice with different values: new='"
                    + serverType + "', old='" + RunningMode.serverType + "'.");
        }
        RunningMode.serverType = serverType;
    }
    public static UserManagement getUserManagement() {
        return userManagement;
    }
    /**
     * After setting all values you should call this method for a logging output with all current settings.
     */
    public static void logMode() {
        log.info("Starting " + Version.getInstance().getAppName() + " " + Version.getInstance().getVersion()
                + " (" + Version.getInstance().formatBuildDateISO(TimeZone.getDefault())
                + ") with: mode='" + RunningMode.getMode() + "', serverType='" + RunningMode.serverType
                + "', home dir='" + ServerConfiguration.getApplicationHome() + "', javaVersion='"
                + System.getProperty("java.version") + "'.");
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java
New file
@@ -0,0 +1,69 @@
package de.micromata.borgbutler.server;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.Transient;
public class ServerConfiguration {
    private static Logger log = LoggerFactory.getLogger(ServerConfiguration.class);
    private final static String[] SUPPORTED_LANGUAGES = {"en"};
    private static String applicationHome;
    private int port;
    private boolean webDevelopmentMode = false;
    private boolean templatesDirModified = false;
    public static ServerConfiguration getDefault() {
        return ServerConfigurationHandler.getDefaultConfiguration();
    }
    public static String[] getSupportedLanguages() {
        return SUPPORTED_LANGUAGES;
    }
    public static String getApplicationHome() {
        if (applicationHome == null) {
            applicationHome = System.getProperty("applicationHome");
            if (StringUtils.isBlank(applicationHome)) {
                applicationHome = System.getProperty("user.dir");
                log.info("applicationHome is not given as JVM   parameter. Using current working dir (OK for start in IDE): " + applicationHome);
            }
        }
        return applicationHome;
    }
    public void resetModifiedFlag() {
        templatesDirModified = false;
    }
    @Transient
    public boolean isTemplatesDirModified() {
        return templatesDirModified;
    }
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    /**
     * If true, CrossOriginFilter will be set.
     */
    public boolean isWebDevelopmentMode() {
        return webDevelopmentMode;
    }
    public void setWebDevelopmentMode(boolean webDevelopmentMode) {
        this.webDevelopmentMode = webDevelopmentMode;
    }
    public void copyFrom(ServerConfiguration other) {
        this.port = other.port;
        this.webDevelopmentMode = other.webDevelopmentMode;
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfigurationHandler.java
New file
@@ -0,0 +1,112 @@
package de.micromata.borgbutler.server;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.Set;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
public class ServerConfigurationHandler {
    private Logger log = LoggerFactory.getLogger(ServerConfigurationHandler.class);
    private static final ServerConfigurationHandler instance = new ServerConfigurationHandler();
    private static final String WEBSERVER_PORT_PREF = "webserver-port";
    public static final int WEBSERVER_PORT_DEFAULT = 8042;
    private static final String LANGUAGE_PREF = "language";
    private static final String LANGUAGE_DEFAULT = null;
    private static final String WEB_DEVELOPMENT_MODE_PREF = "web-development-mode";
    private static final boolean WEB_DEVELOPMENT_MODE_PREF_DEFAULT = false;
    private Preferences preferences;
    private ServerConfiguration configuration = new ServerConfiguration();
    private Set<String> extraPreferences = new HashSet<>();
    /**
     * Only for test case.
     *
     * @param preferences
     */
    ServerConfigurationHandler(Preferences preferences) {
        this.preferences = preferences;
    }
    private ServerConfigurationHandler() {
        preferences = Preferences.userRoot().node("de").node("micromata").node("merlin");
        load();
    }
    public static ServerConfigurationHandler getInstance() {
        return instance;
    }
    public static ServerConfiguration getDefaultConfiguration() {
        return instance.getConfiguration();
    }
    public ServerConfiguration getConfiguration() {
        return configuration;
    }
    public void load() {
        configuration.setPort(preferences.getInt(WEBSERVER_PORT_PREF, WEBSERVER_PORT_DEFAULT));
        configuration.setWebDevelopmentMode(preferences.getBoolean(WEB_DEVELOPMENT_MODE_PREF, WEB_DEVELOPMENT_MODE_PREF_DEFAULT));
        configuration.resetModifiedFlag();
    }
    public void save() {
        log.info("Saving configuration to user prefs.");
        preferences.putInt(WEBSERVER_PORT_PREF, configuration.getPort());
        preferences.putBoolean(WEB_DEVELOPMENT_MODE_PREF, configuration.isWebDevelopmentMode());
        try {
            preferences.flush();
        } catch (BackingStoreException ex) {
            log.error("Couldn't flush user preferences: " + ex.getMessage(), ex);
        }
    }
    /**
     * For saving own properties.
     *
     * @param key   The key under which to save the given value.
     * @param value The value to store. If null, any previous stored value under the given key will be removed.
     */
    public void save(String key, String value) {
        if (StringUtils.isEmpty(value)) {
            preferences.remove(key);
        } else {
            preferences.put(key, value);
            extraPreferences.add(key);
        }
        try {
            preferences.flush();
        } catch (BackingStoreException ex) {
            log.error("Couldn't flush user preferences: " + ex.getMessage(), ex);
        }
    }
    /**
     * @param key Gets own property saved with {@link #save()}.
     */
    public String get(String key, String defaultValue) {
        extraPreferences.add(key);
        return preferences.get(key, defaultValue);
    }
    public void removeAllSettings() {
        log.warn("Removes all configuration settings from user prefs.");
        preferences.remove(WEBSERVER_PORT_PREF);
        preferences.remove(LANGUAGE_PREF);
        preferences.remove(WEB_DEVELOPMENT_MODE_PREF);
        for(String extraKey : extraPreferences) {
            preferences.remove(extraKey);
        }
        try {
            preferences.flush();
        } catch (BackingStoreException ex) {
            log.error("Couldn't flush user preferences: " + ex.getMessage(), ex);
        }
        load();
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/Version.java
New file
@@ -0,0 +1,107 @@
package de.micromata.borgbutler.server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.util.TimeZone;
public class Version {
    private Logger log = LoggerFactory.getLogger(Version.class);
    private String appName;
    private String version;
    private String buildDateUTC;
    private Date buildDate;
    private String updateVersion;
    private static final Version instance = new Version();
    public static Version getInstance() {
        instance.init();
        return instance;
    }
    private void init() {
        synchronized (this) {
            if (appName != null) {
                return;
            }
            try (InputStream inputStream = ClassLoader.getSystemResourceAsStream("version.properties")) {
                if (inputStream == null) {
                    log.warn("version.properties not found (OK, if started e. g. in IDE");
                    version = "99.0";
                    appName = "BorgButler";
                    buildDate = new Date();
                } else {
                    Properties props = new Properties();
                    props.load(inputStream);
                    appName = props.getProperty("name");
                    version = props.getProperty("version");
                    String buildDateMillisString = props.getProperty("build.date.millis");
                    long buildDateMillis = 0;
                    if (buildDateMillisString != null) {
                        try {
                            buildDateMillis = Long.parseLong(buildDateMillisString);
                        } catch (NumberFormatException ex) {
                            log.error("Can't parse build date (millis expected): " + buildDateMillisString + ": " + ex.getMessage(), ex);
                        }
                    }
                    buildDate = new Date(buildDateMillis);
                }
            } catch (Exception ex) {
                log.error("Can't load version information from classpath. File 'version.properties' not found: " + ex.getMessage(), ex);
                appName = "BorgButler";
                version = "?.?";
                buildDateUTC = "1970-01-01 00:00:00";
                return;
            }
            buildDateUTC = formatBuildDateISO(TimeZone.getTimeZone("UTC"));
            log.debug("appName=" + appName + ", version=" + version + ", buildDateUTC=" + buildDateUTC);
        }
    }
    public String formatBuildDateISO(TimeZone timeZone) {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss");
        formatter.setTimeZone(timeZone);
        return formatter.format(buildDate);
    }
    public String getAppName() {
        return appName;
    }
    public String getVersion() {
        return version;
    }
    /**
     * Replaces -SNAPSHOT by dev for snapshot versions. For none snapshot releases the returned value is equal to the version.
     *
     * @return the version as string.
     */
    public String getShortVersion() {
        return version.replace("-SNAPSHOT", "dev");
    }
    public String getBuildDateUTC() {
        return buildDateUTC;
    }
    public Date getBuildDate() {
        return buildDate;
    }
    /**
     * @return Version of the available update, if exist. Otherwise null.
     */
    public String getUpdateVersion() {
        return updateVersion;
    }
    public void setUpdateVersion(String updateVersion) {
        this.updateVersion = updateVersion;
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/jetty/JettyServer.java
New file
@@ -0,0 +1,177 @@
package de.micromata.borgbutler.server.jetty;
import de.micromata.borgbutler.server.ServerConfiguration;
import de.micromata.borgbutler.server.ServerConfigurationHandler;
import de.micromata.borgbutler.server.RunningMode;
import de.micromata.borgbutler.server.rest.ConfigurationRest;
import de.micromata.borgbutler.server.user.UserFilter;
import org.apache.commons.lang3.ArrayUtils;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.*;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.eclipse.jetty.util.resource.Resource;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.DispatcherType;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
public class JettyServer {
    private Logger log = LoggerFactory.getLogger(JettyServer.class);
    private static final String HOST = "127.0.0.1";
    private static final int MAX_PORT_NUMBER = 65535;
    private Server server;
    private int port;
    public void start(String... restPackageNames) {
        port = findFreePort();
        if (port == -1) {
            return;
        }
        log.info("Starting web server on port " + port);
        server = new Server();
        ServerConnector connector = new ServerConnector(server);
        connector.setHost(HOST);
        connector.setPort(port);
        server.setConnectors(new Connector[]{connector});
        ServletContextHandler ctx =
                new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
        ctx.setContextPath("/");
        ResourceConfig resourceConfig = new ResourceConfig();
        String[] packageNames = {ConfigurationRest.class.getPackage().getName()};
        if (restPackageNames != null && restPackageNames.length > 0) {
            packageNames = (String[]) ArrayUtils.addAll(packageNames, restPackageNames);
        }
        resourceConfig.packages(packageNames);
        resourceConfig.register(MultiPartFeature.class)
                .register(JacksonFeature.class);
        //   .register(LoggingFilter.class)
        //   .property("jersey.config.server.tracing.type", "ALL")
        //   .property("jersey.config.server.tracing.threshold", "VERBOSE"))
        ServletHolder jerseyServlet = new ServletHolder(
                new ServletContainer(resourceConfig));
        jerseyServlet.setInitOrder(1);
        ctx.addServlet(jerseyServlet, "/rest/*");
        ctx.addFilter(UserFilter.class, "/rest/*", EnumSet.of(DispatcherType.INCLUDE, DispatcherType.REQUEST));
        // Following code doesn't work:
        // jerseyServlet.setInitParameter("useFileMappedBuffer", "false");
        // jerseyServlet.setInitParameter("cacheControl","max-age=0,public");
        try {
            Path path;
            if (RunningMode.isDevelopmentMode()) {
                path = Paths.get(ServerConfiguration.getApplicationHome(), "merlin-webapp", "build");
            } else {
                path = Paths.get(ServerConfiguration.getApplicationHome(), "web");
            }
            if (!Files.exists(path)) {
                log.error("********** Fatal: Can't find web archive: " + path.toAbsolutePath());
            }
            URL url = path.toUri().toURL();
            log.debug("Using web directory: " + url);
            ctx.setBaseResource(Resource.newResource(url));
        } catch (IOException ex) {
            log.error(ex.getMessage(), ex);
            return;
        }
        ctx.setWelcomeFiles(new String[]{"index.html"});
        ctx.setInitParameter(DefaultServlet.CONTEXT_INIT + "cacheControl", "no-store,no-cache,must-revalidate");//"max-age=5,public");
        ctx.setInitParameter(DefaultServlet.CONTEXT_INIT + "useFileMappedBuffer", "false");
        ctx.addServlet(DefaultServlet.class, "/");
        ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler();
        errorHandler.addErrorPage(404, "/");
        ctx.setErrorHandler(errorHandler);
        if (RunningMode.isDevelopmentMode() || ServerConfigurationHandler.getDefaultConfiguration().isWebDevelopmentMode()) {
            log.warn("*********************************");
            log.warn("***********            **********");
            log.warn("*********** ATTENTION! **********");
            log.warn("***********            **********");
            log.warn("*********** Running in **********");
            log.warn("*********** dev mode!  **********");
            log.warn("***********            **********");
            log.warn("*********************************");
            log.warn("Don't deliver this app in dev mode due to security reasons (CrossOriginFilter is set)!");
            FilterHolder filterHolder = ctx.addFilter(CrossOriginFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
            filterHolder.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");
            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,POST,HEAD");
            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "X-Requested-With,Content-Type,Accept,Origin");
        }
        server.setHandler(ctx);
        try {
            server.start();
        } catch (Exception ex) {
            log.error("Can't start jetty: " + ex.getMessage(), ex);
        }
    }
    public void stop() {
        log.info("Stopping web server.");
        try {
            server.stop();
        } catch (Exception ex) {
            log.error("Can't stop web server: " + ex.getMessage(), ex);
        }
        if (server != null) {
            server.destroy();
        }
    }
    private int findFreePort() {
        int port = ServerConfigurationHandler.getInstance().getConfiguration().getPort();
        return findFreePort(port);
    }
    private int findFreePort(int startPort) {
        int port = startPort > 0 ? startPort : 1;
        if (port > MAX_PORT_NUMBER) {
            log.warn("Port can't be higher than " + MAX_PORT_NUMBER + ": " + port + ". It's a possible mis-configuration.");
            port = ServerConfigurationHandler.WEBSERVER_PORT_DEFAULT;
        }
        for (int i = port; i < port + 10; i++) {
            try (ServerSocket socket = new ServerSocket()) {
                socket.bind(new InetSocketAddress(HOST, i));
                return i;
            } catch (Exception ex) {
                log.info("Port " + i + " already in use or not available. Trying next port.");
                continue; // try next port
            }
        }
        if (startPort != ServerConfigurationHandler.WEBSERVER_PORT_DEFAULT) {
            log.info("Trying to fix port due to a possible mis-configuration.");
            return findFreePort(ServerConfigurationHandler.WEBSERVER_PORT_DEFAULT);
        }
        log.error("No free port found! Giving up.");
        return -1;
    }
    public int getPort() {
        return port;
    }
    public String getUrl() {
        return "http://" + HOST + ":" + port + "/";
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/Log4jMemoryAppender.java
New file
@@ -0,0 +1,136 @@
package de.micromata.borgbutler.server.logging;
import org.apache.commons.collections4.queue.CircularFifoQueue;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.spi.LoggingEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class Log4jMemoryAppender extends AppenderSkeleton {
    private static final int MAX_RESULT_SIZE = 1000;
    private static final int QUEUE_SIZE = 10000;
    private static Log4jMemoryAppender instance;
    private int lastLogEntryOrderNumber = -1;
    public static Log4jMemoryAppender getInstance() {
        return instance;
    }
    public Log4jMemoryAppender() {
        if (instance != null) {
            throw new IllegalArgumentException("Log4jMemoryAppender shouldn't be instantiated twice!");
        }
        instance = this;
    }
    /**
     * For test purposes.
     */
    Log4jMemoryAppender(boolean ignoreMultipleInstance) {
    }
    CircularFifoQueue<LoggingEventData> queue = new CircularFifoQueue<>(QUEUE_SIZE);
    @Override
    protected void append(LoggingEvent event) {
        LoggingEventData eventData = new LoggingEventData(event);
        eventData.orderNumber = ++lastLogEntryOrderNumber;
        queue.add(eventData);
    }
    /**
     * For testing purposes.
     *
     * @param event
     */
    void append(LoggingEventData event) {
        queue.add(event);
    }
    public List<LoggingEventData> query(LogFilter filter, Locale locale) {
        List<LoggingEventData> result = new ArrayList<>();
        if (filter == null) {
            return result;
        }
        int maxSize = filter.getMaxSize() != null ? filter.getMaxSize() : MAX_RESULT_SIZE;
        if (maxSize > MAX_RESULT_SIZE) {
            maxSize = MAX_RESULT_SIZE;
        }
        int counter = 0;
        //I18n i18n = CoreI18n.getDefault().get(locale);
        for (LoggingEventData event : queue) {
            if (!event.getLevel().matches(filter.getThreshold())) {
                continue;
            }
            if (filter.getLastReceivedLogOrderNumber() != null) {
                if (event.getOrderNumber() <= filter.getLastReceivedLogOrderNumber()) {
                    continue;
                }
            }
            String logString = null;
            String message = event.getMessage();
            boolean localizedMessage = false;
            /*if (message != null && message.startsWith("i18n=")) {
                I18nLogEntry i18nLogEntry = I18nLogEntry.parse(message);
                message = i18n.formatMessage(i18nLogEntry.getI18nKey(), (Object[])i18nLogEntry.getArgs());
                localizedMessage = true;
            }*/
            if (StringUtils.isNotBlank(filter.getSearch())) {
                StringBuilder sb = new StringBuilder();
                sb.append(event.getLogDate());
                append(sb, event.getLevel(), true);
                append(sb, message, true);
                append(sb, event.getJavaClass(), true);
                append(sb, event.getStackTrace(), filter.isShowStackTraces());
                logString = sb.toString();
            }
            if (logString == null || matches(logString, filter.getSearch())) {
                LoggingEventData resultEvent = event;
                if (localizedMessage) {
                    // Need a clone
                    resultEvent = (LoggingEventData)event.clone();
                    resultEvent.setMessage(message);
                }
                if (filter.isAscendingOrder()) {
                    result.add(resultEvent);
                } else {
                    result.add(0, resultEvent);
                }
                if (counter++ > maxSize) {
                    break;
                }
            }
        }
        return result;
    }
    private void append(StringBuilder sb, Object value, boolean append) {
        if (!append || value == null) {
            return;
        }
        sb.append("|#|").append(value);
    }
    public void close() {
    }
    public boolean requiresLayout() {
        return false;
    }
    private boolean matches(String str, String searchString) {
        if (StringUtils.isBlank(str)) {
            return StringUtils.isBlank(searchString);
        }
        if (StringUtils.isBlank(searchString)) {
            return true;
        }
        return str.toLowerCase().contains(searchString.toLowerCase());
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogFilter.java
New file
@@ -0,0 +1,71 @@
package de.micromata.borgbutler.server.logging;
/**
 * For filtering log messages.
 */
public class LogFilter {
    private String search;
    private LogLevel threshold;
    private Integer maxSize;
    private boolean ascendingOrder;
    private boolean showStackTraces;
    private Integer lastReceivedLogOrderNumber;
    /**
     * @return Search string for all fields.
     */
    public String getSearch() {
        return search;
    }
    public void setSearch(String search) {
        this.search = search;
    }
    public LogLevel getThreshold() {
        return threshold;
    }
    public void setThreshold(LogLevel threshold) {
        this.threshold = threshold;
    }
    public Integer getMaxSize() {
        return maxSize;
    }
    public void setMaxSize(Integer maxSize) {
        this.maxSize = maxSize;
    }
    public void setAscendingOrder(boolean ascendingOrder) {
        this.ascendingOrder = ascendingOrder;
    }
    /**
     * @return false at default (default is descending order of the result).
     */
    public boolean isAscendingOrder() {
        return ascendingOrder;
    }
    public boolean isShowStackTraces() {
        return showStackTraces;
    }
    public void setShowStackTraces(boolean showStackTraces) {
        this.showStackTraces = showStackTraces;
    }
    /**
     * @return If given, all log entries with order orderNumber higher than this orderNumber will be queried.
     */
    public Integer getLastReceivedLogOrderNumber() {
        return lastReceivedLogOrderNumber;
    }
    public void setLastReceivedLogOrderNumber(Integer lastReceivedLogOrderNumber) {
        this.lastReceivedLogOrderNumber = lastReceivedLogOrderNumber;
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java
New file
@@ -0,0 +1,42 @@
package de.micromata.borgbutler.server.logging;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Level;
import org.apache.log4j.spi.LoggingEvent;
public enum LogLevel {
    ERROR, WARN, INFO, DEBUG, TRACE;
    /**
     * @param treshold
     * @return True, if this log level is equals or higher than given treshold. ERROR is the highest and TRACE the lowest.
     */
    public boolean matches(LogLevel treshold) {
        if (treshold == null) {
            return true;
        }
        return this.ordinal() <= treshold.ordinal();
    }
    public static LogLevel getLevel(LoggingEvent event) {
        switch (event.getLevel().toInt()) {
            case Level.ERROR_INT:
                return LogLevel.ERROR;
            case Level.INFO_INT:
                return LogLevel.INFO;
            case Level.DEBUG_INT:
                return LogLevel.DEBUG;
            case Level.WARN_INT:
                return LogLevel.WARN;
            case Level.TRACE_INT:
                return LogLevel.TRACE;
            default:
                return LogLevel.ERROR;
        }
    }
    public static String getSupportedValues() {
        return StringUtils.join(LogLevel.values(), ", ");
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java
New file
@@ -0,0 +1,121 @@
package de.micromata.borgbutler.server.logging;
import org.apache.commons.lang3.ClassUtils;
import org.apache.log4j.spi.LocationInfo;
import org.apache.log4j.spi.LoggingEvent;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
 * For easier serialization: JSON
 */
public class LoggingEventData implements Cloneable {
    private SimpleDateFormat ISO_DATEFORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    int orderNumber;
    LogLevel level;
    String message;
    private String messageObjectClass;
    private String loggerName;
    private String logDate;
    String javaClass;
    private String javaClassSimpleName;
    private String lineNumber;
    private String methodName;
    private String stackTrace;
    LoggingEventData() {
    }
    public LoggingEventData(LoggingEvent event) {
        level = LogLevel.getLevel(event);
        message = event.getRenderedMessage();
        messageObjectClass = event.getMessage().getClass().toString();
        loggerName = event.getLoggerName();
        logDate = getIsoLogDate(event.timeStamp);
        LocationInfo info = event.getLocationInformation();
        Throwable throwable = event.getThrowableInformation() != null ? event.getThrowableInformation().getThrowable() : null;
        if (throwable != null) {
            StringWriter writer = new StringWriter();
            PrintWriter printWriter = new PrintWriter(writer);
            throwable.printStackTrace(printWriter);
            stackTrace = writer.toString();
        }
        if (info != null) {
            javaClass = info.getClassName();
            javaClassSimpleName = ClassUtils.getShortClassName(info.getClassName());
            lineNumber = info.getLineNumber();
            methodName = info.getMethodName();
        }
    }
    public LogLevel getLevel() {
        return level;
    }
    public String getMessage() {
        return message;
    }
    public String getMessageObjectClass() {
        return messageObjectClass;
    }
    public String getLoggerName() {
        return loggerName;
    }
    public String getLogDate() {
        return logDate;
    }
    public String getJavaClass() {
        return javaClass;
    }
    public String getJavaClassSimpleName() {
        return javaClassSimpleName;
    }
    public String getLineNumber() {
        return lineNumber;
    }
    public String getMethodName() {
        return methodName;
    }
    public int getOrderNumber() {
        return orderNumber;
    }
    public String getStackTrace() {
        return stackTrace;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    private String getIsoLogDate(long millis) {
        synchronized (ISO_DATEFORMAT) {
            return ISO_DATEFORMAT.format(new Date(millis));
        }
    }
    @Override
    public Object clone() {
        LoggingEventData clone = null;
        try {
            clone = (LoggingEventData) super.clone();
        } catch (CloneNotSupportedException ex) {
            throw new UnsupportedOperationException(this.getClass().getCanonicalName() + " isn't cloneable: " + ex.getMessage(), ex);
        }
        return clone;
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java
New file
@@ -0,0 +1,87 @@
package de.micromata.borgbutler.server.rest;
import de.micromata.borgbutler.json.JsonUtils;
import de.micromata.borgbutler.server.ServerConfiguration;
import de.micromata.borgbutler.server.ServerConfigurationHandler;
import de.micromata.borgbutler.server.user.UserData;
import de.micromata.borgbutler.server.user.UserManager;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
@Path("/configuration")
public class ConfigurationRest {
    private Logger log = LoggerFactory.getLogger(ConfigurationRest.class);
    @GET
    @Path("config")
    @Produces(MediaType.APPLICATION_JSON)
    /**
     *
     * @param prettyPrinter If true then the json output will be in pretty format.
     * @see JsonUtils#toJson(Object, boolean)
     */
    public String getConfig(@QueryParam("prettyPrinter") boolean prettyPrinter) {
        ServerConfiguration config = new ServerConfiguration();
        config.copyFrom(ServerConfigurationHandler.getInstance().getConfiguration());
        String json = JsonUtils.toJson(config, prettyPrinter);
        return json;
    }
    @POST
    @Path("config")
    @Produces(MediaType.TEXT_PLAIN)
    public void setConfig(String jsonConfig) {
        ServerConfigurationHandler configurationHandler = ServerConfigurationHandler.getInstance();
        ServerConfiguration config = configurationHandler.getConfiguration();
        ServerConfiguration srcConfig = JsonUtils.fromJson(ServerConfiguration.class, jsonConfig);
        config.copyFrom(srcConfig);
        configurationHandler.save();
    }
    @GET
    @Path("user")
    @Produces(MediaType.APPLICATION_JSON)
    /**
     *
     * @param prettyPrinter If true then the json output will be in pretty format.
     * @see JsonUtils#toJson(Object, boolean)
     */
    public String getUser(@QueryParam("prettyPrinter") boolean prettyPrinter) {
        UserData user = RestUtils.getUser();
        String json = JsonUtils.toJson(user, prettyPrinter);
        return json;
    }
    @POST
    @Path("user")
    @Produces(MediaType.TEXT_PLAIN)
    public void setUser(String jsonConfig) {
        UserData user = JsonUtils.fromJson(UserData.class, jsonConfig);
        if (user.getLocale() != null && StringUtils.isBlank(user.getLocale().getLanguage())) {
            // Don't set locale with "" as language.
            user.setLocale(null);
        }
        if (StringUtils.isBlank(user.getDateFormat())) {
            // Don't set dateFormat as "".
            user.setDateFormat(null);
        }
        UserManager.instance().saveUser(user);
    }
    /**
     * Resets the settings to default values (deletes all settings).
     */
    @GET
    @Path("reset")
    @Produces(MediaType.APPLICATION_JSON)
    public String resetConfig(@QueryParam("IKnowWhatImDoing") boolean securityQuestion) {
        if (securityQuestion) {
            ServerConfigurationHandler.getInstance().removeAllSettings();
        }
        return getConfig(false);
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java
New file
@@ -0,0 +1,41 @@
package de.micromata.borgbutler.server.rest;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.util.Locale;
@Path("/i18n")
public class I18nRest {
    private Logger log = LoggerFactory.getLogger(I18nRest.class);
    @GET
    @Path("list")
    @Produces(MediaType.APPLICATION_JSON)
    /**
     *
     * @param requestContext For detecting the user's client locale.
     * @param locale If not given, the client's language (browser) will be used.
     * @param keysOnly If true, only the keys will be returned. Default is false.
     * @param prettyPrinter If true then the json output will be in pretty format.
     * @see JsonUtils#toJson(Object, boolean)
     */
    public String getList(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter,
                          @QueryParam("keysOnly") boolean keysOnly, @QueryParam("locale") String locale) {
        Locale localeObject;
        if (StringUtils.isNotBlank(locale)) {
            localeObject = new Locale(locale);
        } else {
            localeObject = RestUtils.getUserLocale(requestContext);
        }
        return ""; // i18n not yet supported.
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/LoggingRest.java
New file
@@ -0,0 +1,70 @@
package de.micromata.borgbutler.server.rest;
import de.micromata.borgbutler.json.JsonUtils;
import de.micromata.borgbutler.server.logging.Log4jMemoryAppender;
import de.micromata.borgbutler.server.logging.LogFilter;
import de.micromata.borgbutler.server.logging.LogLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@Path("/logging")
public class LoggingRest {
    private Logger log = LoggerFactory.getLogger(LoggingRest.class);
    /**
     * @param requestContext
     * @param search
     * @param logLevelTreshold fatal, error, warn, info, debug or trace (case insensitive).
     * @param maxSize          Max size of the result list.
     * @param ascendingOrder   Default is false (default is descending order).
     * @param lastReceivedOrderNumber The last received order number for updating log entries (preventing querying all entries again).
     * @param mdcTemplatePk
     * @param mdcTemplateDefinitionPk
     * @param prettyPrinter
     * @return
     */
    @GET
    @Path("query")
    @Produces(MediaType.APPLICATION_JSON)
    public String query(@Context HttpServletRequest requestContext,
                        @QueryParam("search") String search, @QueryParam("treshold") String logLevelTreshold,
                        @QueryParam("maxSize") Integer maxSize, @QueryParam("ascendingOrder") Boolean ascendingOrder,
                        @QueryParam("lastReceivedOrderNumber") Integer lastReceivedOrderNumber,
                        @QueryParam("mdcTemplatePk") String mdcTemplatePk,
                        @QueryParam("mdcTemplateDefinitionPk") String mdcTemplateDefinitionPk,
                        @QueryParam("prettyPrinter") boolean prettyPrinter) {
        LogFilter filter = new LogFilter();
        filter.setSearch(search);
        if (logLevelTreshold != null) {
            try {
                LogLevel treshold = LogLevel.valueOf(logLevelTreshold.trim().toUpperCase());
                filter.setThreshold(treshold);
            } catch (IllegalArgumentException ex) {
                log.error("Can't parse log level treshold: " + logLevelTreshold + ". Supported values (case insensitive): " + LogLevel.getSupportedValues());
            }
        }
        if (filter.getThreshold() == null) {
            filter.setThreshold(LogLevel.INFO);
        }
        if (maxSize != null) {
            filter.setMaxSize(maxSize);
        }
        if (ascendingOrder != null && ascendingOrder == true) {
            filter.setAscendingOrder(true);
        }
        if (lastReceivedOrderNumber != null) {
            filter.setLastReceivedLogOrderNumber(lastReceivedOrderNumber);
        }
        Log4jMemoryAppender appender = Log4jMemoryAppender.getInstance();
        String json = JsonUtils.toJson(appender.query(filter, RestUtils.getUserLocale(requestContext)), prettyPrinter);
        return json;
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/RestUtils.java
New file
@@ -0,0 +1,58 @@
package de.micromata.borgbutler.server.rest;
import de.micromata.borgbutler.server.RunningMode;
import de.micromata.borgbutler.server.user.UserData;
import de.micromata.borgbutler.server.user.UserUtils;
import org.slf4j.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
import java.util.Locale;
public class RestUtils {
    /**
     * @return null, if the local app (JavaFX) is running and the request is from localhost. Otherwise message, why local
     * service isn't available.
     */
    public static String checkLocalDesktopAvailable(HttpServletRequest requestContext) {
        if (RunningMode.getServerType() != RunningMode.ServerType.DESKTOP) {
            return "Service unavailable. No desktop app on localhost available.";
        }
        String remoteAddr = requestContext.getRemoteAddr();
        if (remoteAddr == null || !remoteAddr.equals("127.0.0.1")) {
            return "Service not available. Can't call this service remote. Run this service on localhost of the running desktop app.";
        }
        return null;
    }
    /**
     * @return Returns the user put by the UserFilter.
     * @see UserUtils#getUser()
     * @see de.micromata.borgbutler.server.user.UserFilter
     */
    static UserData getUser() {
        UserData user = UserUtils.getUser();
        if (user == null) {
            throw new IllegalStateException("No user given in rest call.");
        }
        return UserUtils.getUser();
    }
    static Locale getUserLocale(HttpServletRequest requestContext) {
        UserData user = RestUtils.getUser();
        Locale locale = user.getLocale();
        if (locale == null) {
            locale = requestContext.getLocale();
        }
        return locale;
    }
    static Response get404Response(Logger log, String errorMessage) {
        log.error(errorMessage);
        Response response = Response.status(404).
                entity(errorMessage).
                type("text/plain").
                build();
        return response;
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java
New file
@@ -0,0 +1,88 @@
package de.micromata.borgbutler.server.rest;
import de.micromata.borgbutler.json.JsonUtils;
import de.micromata.borgbutler.server.Languages;
import de.micromata.borgbutler.server.Version;
import de.micromata.borgbutler.server.user.UserData;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.util.Date;
import java.util.Locale;
@Path("/")
public class VersionRest {
    private Logger log = LoggerFactory.getLogger(VersionRest.class);
    @GET
    @Path("version")
    @Produces(MediaType.APPLICATION_JSON)
    /**
     *
     * @param requestContext For detecting the user's client locale.
     * @param prettyPrinter If true then the json output will be in pretty format.
     * @see JsonUtils#toJson(Object, boolean)
     */
    public String getVersion(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter) {
        UserData user = RestUtils.getUser();
        String language = Languages.asString(user.getLocale());
        if (StringUtils.isBlank(language)) {
            Locale locale = requestContext.getLocale();
            language = locale.getLanguage();
        }
        MyVersion version = new MyVersion(language, RestUtils.checkLocalDesktopAvailable(requestContext) == null);
        String json = JsonUtils.toJson(version, prettyPrinter);
        return json;
    }
    public class MyVersion {
        private Version version;
        private String language;
        private boolean localDesktopAvailable;
        private MyVersion(String language, boolean localDesktopAvailable) {
            this.version = Version.getInstance();
            this.language = language;
            this.localDesktopAvailable = localDesktopAvailable;
        }
        public String getAppName() {
            return version.getAppName();
        }
        public String getVersion() {
            return version.getVersion();
        }
        public String getBuildDateUTC() {
            return version.getBuildDateUTC();
        }
        public Date getBuildDate() {
            return version.getBuildDate();
        }
        /**
         * @return Version of the available update, if exist. Otherwise null.
         */
        public String getUpdateVersion() {
            return version.getUpdateVersion();
        }
        public String getLanguage() {
            return language;
        }
        public boolean isLocalDesktopAvailable() {
            return localDesktopAvailable;
        }
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java
New file
@@ -0,0 +1,55 @@
package de.micromata.borgbutler.server.user;
import de.micromata.borgbutler.server.Languages;
import de.micromata.borgbutler.server.RunningMode;
import de.micromata.borgbutler.server.ServerConfigurationHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
/**
 * Contains only one (dummy) user (for desktop version).
 */
public class SingleUserManager extends UserManager {
    private static final String USER_LOCAL_PREF_KEY = "userLocale";
    private static Logger log = LoggerFactory.getLogger(SingleUserManager.class);
    private UserData singleUser;
    public SingleUserManager() {
        if (RunningMode.getUserManagement() != RunningMode.UserManagement.SINGLE) {
            throw new IllegalStateException("Can't use SingleUserManager in user management mode '" + RunningMode.getUserManagement()
                    + "'. Only allowed in '" + RunningMode.UserManagement.SINGLE + "'.");
        }
        log.info("Using SingleUserManger as user manager.");
        singleUser = new UserData();
        singleUser.setUsername("admin");
        singleUser.setAdmin(true);
        String language = ServerConfigurationHandler.getInstance().get("userLocale", null);
        Locale locale = Languages.asLocale(language);
        singleUser.setLocale(locale);
        String dateFormat = ServerConfigurationHandler.getInstance().get("userDateFormat", null);
        singleUser.setDateFormat(dateFormat);
    }
    public UserData getUser(String id) {
        return singleUser;
    }
    /**
     * Stores only the user's configured locale.
     *
     * @param userData
     * @see ServerConfigurationHandler#save(String, String)
     */
    @Override
    public void saveUser(UserData userData) {
        Locale locale = userData.getLocale();
        this.singleUser.setLocale(locale);
        String dateFormat = userData.getDateFormat();
        this.singleUser.setDateFormat(dateFormat);
        String lang = Languages.asString(locale);
        ServerConfigurationHandler.getInstance().save("userLocale", lang);
        ServerConfigurationHandler.getInstance().save("userDateFormat", dateFormat);
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserData.java
New file
@@ -0,0 +1,59 @@
package de.micromata.borgbutler.server.user;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.Locale;
/**
 * Stores the user data in Thread local for accessing everywhere inside the rest thread.
 * It's only a dummy and simple implementation.
 */
public class UserData {
    private Locale locale;
    private String username;
    private String dateFormat;
    private boolean admin;
    public Locale getLocale() {
        return locale;
    }
    public void setLocale(Locale locale) {
        this.locale = locale;
    }
    public String getDateFormat() {
        return dateFormat;
    }
    public void setDateFormat(String dateFormat) {
        this.dateFormat = dateFormat;
    }
    public String getUsername() {
        return username;
    }
    void setUsername(String username) {
        this.username = username;
    }
    public boolean isAdmin() {
        return admin;
    }
    public void setAdmin(boolean admin) {
        this.admin = admin;
    }
    @Override
    public String toString() {
        ToStringBuilder tos = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
        tos.append("username", username);
        tos.append("dateFormat", dateFormat);
        tos.append("admin", admin);
        tos.append("locale", locale);
        return tos.toString();
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java
New file
@@ -0,0 +1,53 @@
package de.micromata.borgbutler.server.user;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
 * Ensuring the user data inside request threads. For now, it's only a simple implementation (no login required).
 * Only the user's (client's) locale is used.
 */
public class UserFilter implements Filter {
    private Logger log = LoggerFactory.getLogger(UserFilter.class);
    @Override
    public void init(FilterConfig filterConfig) {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        try {
            UserData userData = UserUtils.getUser();
            if (userData != null) {
                log.warn("****************************************");
                log.warn("***********                   **********");
                log.warn("*********** SECURITY WARNING! **********");
                log.warn("***********                   **********");
                log.warn("*********** Internal error:   **********");
                log.warn("*********** User already set! **********");
                log.warn("***********                   **********");
                log.warn("****************************************");
                log.warn("Don't deliver this app in dev mode due to security reasons!");
                String message = "User already given for this request. Rejecting request due to security reasons. Given user: " + userData;
                log.error(message);
                throw new IllegalArgumentException(message);
            }
            userData = UserManager.instance().getUser("dummy");
            UserUtils.setUser(userData, request.getLocale());
            if (log.isDebugEnabled()) log.debug("Request for user: " + userData);
            chain.doFilter(request, response);
        } finally {
            UserUtils.removeUser();
        }
    }
    @Override
    public void destroy() {
    }
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserManager.java
New file
@@ -0,0 +1,24 @@
package de.micromata.borgbutler.server.user;
/**
 * Handles all user data.
 */
public abstract class UserManager {
    private static UserManager instance;
    public static void setUserManager(UserManager userManager) {
        instance = userManager;
    }
    public static UserManager instance() {
        return instance;
    }
    public abstract UserData getUser(String id);
    /**
     * The userData was modified. Persists the given user.
     * @param userData
     */
    public abstract void saveUser(UserData userData);
}
borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserUtils.java
New file
@@ -0,0 +1,53 @@
package de.micromata.borgbutler.server.user;
import java.util.Locale;
public class UserUtils {
    private static final ThreadLocal<UserInfo> threadUserInfo = new ThreadLocal<UserInfo>();
    /**
     * Gets the current user from ThreadLocal.
     * @return
     */
    public static UserData getUser() {
        UserInfo user = threadUserInfo.get();
        if (user == null) {
            return null;
        }
        return user.userData;
    }
    public static Locale getUserLocale() {
        UserInfo userInfo = threadUserInfo.get();
        if (userInfo == null) return null;
        UserData user = userInfo.userData;
        Locale locale = user.getLocale();
        if (locale == null) {
            locale = userInfo.requestLocale;
        }
        return locale;
    }
    public static String getUserDateFormat() {
        UserData user = getUser();
        return user != null ? user.getDateFormat() : null;
    }
    static void setUser(UserData user, Locale requestLocale) {
        threadUserInfo.set(new UserInfo(user, requestLocale));
    }
    static void removeUser() {
        threadUserInfo.remove();
    }
    private static class UserInfo {
        private UserData userData;
        private Locale requestLocale;
        private UserInfo(UserData userData, Locale requestLocale) {
            this.userData = userData;
            this.requestLocale = requestLocale;
        }
    }
}
borgbutler-server/src/main/resources/log4j.properties
New file
@@ -0,0 +1,19 @@
log4j.rootLogger=info, stdout, memory, file
#log4j.logger.de.micromata.borgbutler.persistency=debug
#log4j.logger.de.micromata.borgbutler.main.jetty=debug
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# Pattern to output the caller's file name and line number.
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
log4j.appender.memory=de.micromata.borgbutler.server.logging.Log4jMemoryAppender
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=merlin.log
log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.MaxBackupIndex=5
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
borgbutler-webapp/build.gradle
New file
@@ -0,0 +1,8 @@
description = 'borgbutler-webapp'
task npmBuild(type: Exec) {
    workingDir '.'
    executable 'sh'
    commandLine 'npm', 'run', 'build'
}
borgbutler-webapp/package.json
New file
@@ -0,0 +1,40 @@
{
  "name": "borgbuttler-webapp",
  "version": "0.1.0",
  "private": false,
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.6",
    "@fortawesome/free-solid-svg-icons": "^5.4.1",
    "@fortawesome/react-fontawesome": "^0.1.3",
    "bootstrap": "^4.1.3",
    "classnames": "^2.2.6",
    "history": "^4.7.2",
    "i": "^0.3.6",
    "js-file-download": "^0.4.4",
    "npm": "^6.4.1",
    "prop-types": "^15.5.7",
    "react": "^16.4.2",
    "react-dom": "^16.4.2",
    "react-highlighter": "^0.4.3",
    "react-redux": "^5.0.7",
    "react-router": "^4.3.1",
    "react-router-bootstrap": "^0.24.4",
    "react-router-dom": "^4.3.1",
    "react-scripts": "^2.0.5",
    "reactstrap": "^6.5.0",
    "redux": "^4.0.0",
    "redux-thunk": "^2.3.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}
borgbutler-webapp/public/browserconfig.xml
New file
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
borgbutler-webapp/public/css/bootstrap.min.css
New file
@@ -0,0 +1,7 @@
/*!
 * Bootstrap v4.1.3 (https://getbootstrap.com/)
 * Copyright 2011-2018 The Bootstrap Authors
 * Copyright 2011-2018 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014 \00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{background-color:#71dd8a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(40,167,69,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label::after,.was-validated .custom-file-input:valid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{background-color:#efa2a9}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(220,53,69,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label::after,.was-validated .custom-file-input:invalid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;background-color:transparent}.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link.focus,.btn-link:focus{text-decoration:underline;border-color:transparent;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media screen and (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media screen and (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-right{right:0;left:auto}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:active~.custom-control-label::before{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#dee2e6}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:focus~.custom-file-label::after{border-color:#80bdff}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.25rem;padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:1px solid #ced4da;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;padding-left:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion .card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion .card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion .card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion .card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media screen and (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:not(:disabled):not(.disabled){cursor:pointer}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{color:#000;text-decoration:none;opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}@media screen and (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-dialog-centered::before{display:block;height:calc(100vh - (.5rem * 2));content:""}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-dialog-centered::before{height:calc(100vh - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-ms-flex-align:center;align-items:center;width:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease}@media screen and (prefers-reduced-motion:reduce){.carousel-item-next,.carousel-item-prev,.carousel-item.active{transition:none}}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-fade .carousel-item{opacity:0;transition-duration:.6s;transition-property:opacity}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{opacity:0}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}
/*# sourceMappingURL=bootstrap.min.css.map */
borgbutler-webapp/public/favicon.ico
Binary files differ
borgbutler-webapp/public/favicon/android-icon-144x144.png
borgbutler-webapp/public/favicon/android-icon-192x192.png
borgbutler-webapp/public/favicon/android-icon-36x36.png
borgbutler-webapp/public/favicon/android-icon-48x48.png
borgbutler-webapp/public/favicon/android-icon-72x72.png
borgbutler-webapp/public/favicon/android-icon-96x96.png
borgbutler-webapp/public/favicon/apple-icon-114x114.png
borgbutler-webapp/public/favicon/apple-icon-120x120.png
borgbutler-webapp/public/favicon/apple-icon-144x144.png
borgbutler-webapp/public/favicon/apple-icon-152x152.png
borgbutler-webapp/public/favicon/apple-icon-180x180.png
borgbutler-webapp/public/favicon/apple-icon-57x57.png
borgbutler-webapp/public/favicon/apple-icon-60x60.png
borgbutler-webapp/public/favicon/apple-icon-72x72.png
borgbutler-webapp/public/favicon/apple-icon-76x76.png
borgbutler-webapp/public/favicon/apple-icon-precomposed.png
borgbutler-webapp/public/favicon/apple-icon.png
borgbutler-webapp/public/favicon/favicon-16x16.png
borgbutler-webapp/public/favicon/favicon-32x32.png
borgbutler-webapp/public/favicon/favicon-96x96.png
borgbutler-webapp/public/favicon/ms-icon-144x144.png
borgbutler-webapp/public/favicon/ms-icon-150x150.png
borgbutler-webapp/public/favicon/ms-icon-310x310.png
borgbutler-webapp/public/favicon/ms-icon-70x70.png
borgbutler-webapp/public/images/merlin-icon.png
borgbutler-webapp/public/index.html
New file
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
    <link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
    <link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
    <link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
    <link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
    <link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
    <link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
    <link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
    <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
    <link rel="icon" type="image/png" sizes="192x192"  href="/favicon/android-icon-192x192.png">
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
    <link rel="manifest" href="/manifest.json">
    <meta name="msapplication-TileColor" content="#ffffff">
    <meta name="msapplication-TileImage" content="/favicon/ms-icon-144x144.png">
    <meta name="theme-color" content="#ffffff">
    <title>Merlin Webapp</title>
<body>
<noscript>
    You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
borgbutler-webapp/public/manifest.json
New file
@@ -0,0 +1,46 @@
{
  "short_name": "Merlin Webapp",
  "name": "Control Merlin with this webapp.",
  "start_url": "./index.html",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
  "icons": [
    {
      "src": "\/android-icon-36x36.png",
      "sizes": "36x36",
      "type": "image\/png",
      "density": "0.75"
    },
    {
      "src": "\/android-icon-48x48.png",
      "sizes": "48x48",
      "type": "image\/png",
      "density": "1.0"
    },
    {
      "src": "\/android-icon-72x72.png",
      "sizes": "72x72",
      "type": "image\/png",
      "density": "1.5"
    },
    {
      "src": "\/android-icon-96x96.png",
      "sizes": "96x96",
      "type": "image\/png",
      "density": "2.0"
    },
    {
      "src": "\/android-icon-144x144.png",
      "sizes": "144x144",
      "type": "image\/png",
      "density": "3.0"
    },
    {
      "src": "\/android-icon-192x192.png",
      "sizes": "192x192",
      "type": "image\/png",
      "density": "4.0"
    }
  ]
}
borgbutler-webapp/src/actions/index.js
New file
@@ -0,0 +1,6 @@
import {changeFilter, requestLogReload} from './log';
import {loadVersion} from './version';
export {changeFilter, requestLogReload};
export {loadVersion};
borgbutler-webapp/src/actions/log.js
New file
@@ -0,0 +1,54 @@
import {getRestServiceUrl} from '../utilities/global';
import {LOG_VIEW_CHANGE_FILTER, LOG_VIEW_FAILED_RELOAD, LOG_VIEW_RELOADED, LOG_VIEW_REQUEST_RELOAD} from './types';
const requestedLogReload = () => ({
    type: LOG_VIEW_REQUEST_RELOAD
});
const reloadedLog = (data) => ({
    type: LOG_VIEW_RELOADED,
    payload: data
});
const failedLogReload = () => ({
    type: LOG_VIEW_FAILED_RELOAD
});
const changedFilter = (name, value) => ({
    type: LOG_VIEW_CHANGE_FILTER,
    payload: {name, value}
});
export const changeFilter = (name, value) => (dispatch) => dispatch(changedFilter(name, value));
const loadLog = (dispatch, getState) => {
    dispatch(requestedLogReload());
    const {filters} = getState().log;
    fetch(getRestServiceUrl('logging/query', {
        search: filters.search,
        treshold: filters.threshold,
        maxSize: filters.maxSize,
        showStackTrace: filters.showStackTrace,
        ascendingOrder: filters.ascendingOrder,
        // TODO ADD lastReceivedOrderNumber
    }), {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json'
        }
    })
        .then(response => response.json())
        .then(json => dispatch(reloadedLog(json.map(entry => ({
            ...entry,
            level: entry.level.toLowerCase()
        })))))
        .catch(() => dispatch(failedLogReload()));
};
export const requestLogReload = () => (dispatch, getState) => {
    if (!getState().log.loading) {
        loadLog(dispatch, getState);
    }
};
borgbutler-webapp/src/actions/types.js
New file
@@ -0,0 +1,8 @@
export const LOG_VIEW_RELOADED = 'LOG_VIEW_RELOADED';
export const LOG_VIEW_REQUEST_RELOAD = 'LOG_VIEW_REQUEST_RELOAD';
export const LOG_VIEW_FAILED_RELOAD = 'LOG_VIEW_FAILED_RELOAD';
export const LOG_VIEW_CHANGE_FILTER = 'LOG_VIEW_CHANGE_FILTER';
export const VERSION_REQUEST_RELOAD = 'VERSION_REQUEST_RELOAD';
export const VERSION_RELOADED = 'VERSION_RELOADED';
export const VERSION_RELOAD_FAILED = 'VERSION_RELOAD_FAILED';
borgbutler-webapp/src/actions/version.js
New file
@@ -0,0 +1,38 @@
import {VERSION_RELOAD_FAILED, VERSION_RELOADED, VERSION_REQUEST_RELOAD} from './types';
import {getRestServiceUrl} from '../utilities/global';
const requestedVersionReload = () => ({
    type: VERSION_REQUEST_RELOAD
});
const reloadedVersion = (json) => ({
    type: VERSION_RELOADED,
    payload: {
        version: json.version,
        language: json.language,
        buildDate: json.buildDate,
        updateVersion: json.updateVersion
    }
});
const failedReload = () => ({
    type: VERSION_RELOAD_FAILED
});
export const loadVersion = () => (dispatch, getState) => {
    if (getState().version.loading) {
        return;
    }
    dispatch(requestedVersionReload());
    fetch(getRestServiceUrl('version'), {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json'
        }
    })
        .then(response => response.json())
        .then(json => dispatch(reloadedVersion(json)))
        .catch(() => dispatch(failedReload()));
};
borgbutler-webapp/src/components/general/BootstrapComponents.jsx
New file
@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
function PageHeader({children}) {
    return (
        <div className={'pb-2 mt-4 mb-2 border-bottom pageheader'}>
            {children}
        </div>
    );
}
PageHeader.propTypes = {
    children: PropTypes.node
};
PageHeader.defaultProps = {
    validationState: 'no-validation',
    children: null
};
export {
    PageHeader
};
borgbutler-webapp/src/components/general/ErrorAlert.jsx
New file
@@ -0,0 +1,20 @@
import React from 'react';
import {Alert} from 'reactstrap';
import {FormButton} from "./forms/FormComponents";
import I18n from "./translation/I18n";
const ErrorAlert = (props) => <Alert
    color={'danger'}
>
    <h4>{props.titleKey ? <I18n name={props.titleKey}/> : props.title}</h4>
    <p>{props.descriptionKey ? <I18n name={props.descriptionKey}/> :props.description}</p>
    {props.action ? <p>
        <FormButton bsStyle={props.action.style}
            onClick={props.action.handleClick}
        >
            {props.action.titleKey ? <I18n name={props.action.titleKey}/> : props.action.title}
        </FormButton>
    </p> : undefined}
</Alert>;
export default ErrorAlert;
borgbutler-webapp/src/components/general/ErrorAlertGenericRestFailure.jsx
New file
@@ -0,0 +1,13 @@
import React from 'react';
import ErrorAlert from './ErrorAlert';
const ErrorAlertGenericRestFailure = (props) => <ErrorAlert
        titleKey={'common.alert.cantLoadData'}
        descriptionKey={'common.alert.genericRestAPIFailure'}
        action={{
            handleClick: props.handleClick,
            titleKey: 'common.alert.tryAgain'
        }}
    />
export default ErrorAlertGenericRestFailure;
borgbutler-webapp/src/components/general/IconComponents.jsx
New file
@@ -0,0 +1,127 @@
import React from 'react';
import {
    faCaretDown,
    faCaretUp,
    faCheck,
    faCircleNotch,
    faDownload,
    faExclamationTriangle,
    faInfoCircle,
    faPlus,
    faSortDown,
    faSortUp,
    faSync,
    faTrash,
    faTimes,
    faSkullCrossbones,
    faUpload
} from '@fortawesome/free-solid-svg-icons'
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
function IconAdd() {
    return (
        <FontAwesomeIcon icon={faPlus}/>
    );
}
function IconCancel() {
    return (
        <FontAwesomeIcon icon={faTimes}/>
    );
}
function IconCheck() {
    return (
        <FontAwesomeIcon icon={faCheck}/>
    );
}
function IconCollapseClose() {
    return (
        <FontAwesomeIcon icon={faCaretUp}/>
    );
}
function IconCollapseOpen() {
    return (
        <FontAwesomeIcon icon={faCaretDown}/>
    );
}
function IconDanger() {
    return (
        <FontAwesomeIcon icon={faSkullCrossbones}/>
    );
}
function IconDownload() {
    return (
        <FontAwesomeIcon icon={faDownload}/>
    );
}
function IconInfo() {
    return (
        <FontAwesomeIcon icon={faInfoCircle}/>
    );
}
function IconRefresh() {
    return (
        <FontAwesomeIcon icon={faSync}/>
    );
}
function IconRemove() {
    return (
        <FontAwesomeIcon icon={faTrash}/>
    );
}
function IconSpinner() {
    return (
        <FontAwesomeIcon icon={faCircleNotch} spin={true} size={'3x'} color={'#aaaaaa'} />
    );
}
function IconSortDown() {
    return (
        <FontAwesomeIcon icon={faSortDown}/>
    );
}
function IconSortUp() {
    return (
        <FontAwesomeIcon icon={faSortUp}/>
    );
}
function IconUpload() {
    return (
        <FontAwesomeIcon icon={faUpload}/>
    );
}
function IconWarning() {
    return (
        <FontAwesomeIcon icon={faExclamationTriangle}/>
    );
}
export {
    IconAdd,
    IconCancel,
    IconCheck,
    IconCollapseClose,
    IconCollapseOpen,
    IconDanger,
    IconDownload,
    IconInfo,
    IconRefresh,
    IconRemove,
    IconSortDown,
    IconSortUp,
    IconSpinner,
    IconUpload,
    IconWarning
};
borgbutler-webapp/src/components/general/LinkFile.jsx
New file
@@ -0,0 +1,41 @@
import React from 'react';
import {Button} from 'reactstrap';
import {getRestServiceUrl} from "../../utilities/global";
/*
* Link to a file. If Merlin is running both local, the server and the client, the file will be opened (e. g. in Excel).
* For remote clients the desired file will be downloaded.
 */
class LinkFile extends React.Component {
    openFile() {
        fetch(getRestServiceUrl('files/open-local-file', {primaryKey: this.props.primaryKey}), {
            method: "GET",
            dataType: "text",
            headers: {
                "Content-Type": "text/plain; charset=utf-8"
            }
        })
            .then(response => response.text())
            .then((text) => {
                if (text !== 'OK') {
                    alert(text);
                }
            })
            .catch((error) => {
                alert("Can't open file on local file system :-(")
            })
    }
    render() {
        return (
            <Button color="link" onClick={() => this.openFile()}>{this.props.filepath}</Button>
        )
    }
    constructor(props) {
        super(props);
        this.openFile = this.openFile.bind(this);
    }
}
export default LinkFile;
borgbutler-webapp/src/components/general/Loading.jsx
New file
@@ -0,0 +1,8 @@
import React from 'react';
import I18n from "./translation/I18n";
const Loading = (props) => <div>
        <i><I18n name={'common.loading'}/></i>
    </div>
export default Loading;
borgbutler-webapp/src/components/general/Menu.jsx
New file
@@ -0,0 +1,89 @@
import React from 'react';
import {NavLink as ReactRouterNavLink} from 'react-router-dom';
import {Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink, UncontrolledTooltip} from 'reactstrap';
import {getResponseHeaderFilename, getRestServiceUrl} from '../../utilities/global';
import fileDownload from 'js-file-download';
import LoadingOverlay from './loading/LoadingOverlay';
import FailedOverlay from './loading/failed/Overlay';
import I18n from "./translation/I18n";
class Menu extends React.Component {
    getNavElement = (route, index) => {
        if (index === 0) {
            return '';
        }
        let addition = '';
        let className = '';
        // Additional Route Settings
        if (route.length >= 4) {
            if (route[3].badge) {
                addition = route[3].badge;
            }
            if (route[3].className) {
                className = route[3].className;
            }
        }
        return (
            <NavItem key={index}>
                <NavLink
                    to={route[1]}
                    tag={ReactRouterNavLink}
                    className={className}
                >
                    {route[0]} {addition}
                </NavLink>
            </NavItem>
        );
    };
    constructor(props) {
        super(props);
        this.toggle = this.toggle.bind(this);
        this.state = {
            loading: false,
            failed: false,
            isOpen: false
        };
        this.uploadFile = this.uploadFile.bind(this);
    }
     toggle() {
        this.setState({
            isOpen: !this.state.isOpen
        });
    }
    render() {
        return (
            <Navbar className={'fixed-top'} color="light" light expand="lg">
                <NavbarBrand to="/" tag={ReactRouterNavLink}><img alt={'BorgButler logo'}
                                                                  src={'../../../images/merlin-icon.png'}
                                                                  width={'50px'}/>BorgButler</NavbarBrand>
                <NavbarToggler onClick={this.toggle}/>
                <Collapse isOpen={this.state.isOpen} navbar>
                    <Nav className="ml-auto" navbar>
                        {
                            this.props.routes.map((route, index) => (
                                this.getNavElement(route, index)
                            ))
                        }
                    </Nav>
                    <DropArea id={'menuDropZone'} className={'menu'}
                              upload={this.uploadFile}
                    />
                </Collapse>
                <LoadingOverlay active={this.state.loading} />
                <FailedOverlay
                    title={'File Upload failed'}
                    text={this.state.failed}
                    closeModal={() => this.setState({failed: false})}
                    active={this.state.failed}
                />
            </Navbar>
        );
    }
}
export default Menu;
borgbutler-webapp/src/components/general/OpenLocalFile.jsx
New file
@@ -0,0 +1,47 @@
import React from 'react';
import {Button} from 'reactstrap';
import {getRestServiceUrl} from "../../utilities/global";
class OpenLocalFile extends React.Component {
    openFile() {
        fetch(getRestServiceUrl('files/open-local-file', {filepath: this.props.filepath}), {
            method: "GET",
            dataType: "text",
            headers: {
                "Content-Type": "text/plain; charset=utf-8"
            }
        })
            .then(response => response.text())
            .then((text) => {
                if (text !== 'OK') {
                    alert(text);
                }
            })
            .catch((error) => {
                alert("Can't open file on local file system :-(")
            })
    }
    render() {
        const service = this.props.service;
        const params = this.props.params;
        var url;
        if (params) {
            if (service === 'files/browse-local-filesystem') {
                url = getRestServiceUrl(service) + '?' + params;
            } else {
                url = getRestServiceUrl(service) + '?prettyPrinter=true&' + params;
            }
        } else {
            url = getRestServiceUrl(service) + '?prettyPrinter=true';
        }
        return (
            <Button color="link" onClick={() => this.openFile()}>{this.props.filepath}</Button>
        )
    }
    constructor(props) {
        super(props);
        this.openFile = this.openFile.bind(this);
    }
}
export default OpenLocalFile;
borgbutler-webapp/src/components/general/forms/EditableTextField.css
New file
@@ -0,0 +1,33 @@
div.editable-text-field {
    width: 100%;
    display: block;
    font-size: 16px;
    margin: 5px 0;
}
div.editable-text-field div.editable-text-div {
    display: block;
    position: relative;
}
div.editable-text-field div.editable-text-field-label {
    font-weight: 700;
}
.editable-text-field-value:hover {
    cursor: pointer;
}
div.editable-text-field div.editable-text-field-value-container {
    width: 100%;
}
div.editable-text-field div.editable-text-field-value-container input {
    font-size: 16px;
}
div.editable-text-field div.editable-text-field-value-container span.editable-text-field-value {
    padding: 6px 12px;
    display: block;
    width: 100%;
}
borgbutler-webapp/src/components/general/forms/EditableTextField.jsx
New file
@@ -0,0 +1,136 @@
import React from 'react';
import './EditableTextField.css';
import {Button, Input, InputGroup, InputGroupAddon} from 'reactstrap';
import {IconCheck, IconCancel} from '../IconComponents'
class EditableTextField extends React.Component {
    state = {
        editing: false
    };
    constructor(props) {
        super(props);
        this.startEditing = this.startEditing.bind(this);
        this.stopEditing = this.stopEditing.bind(this);
    }
    startEditing = () => {
        this.setState({
            editing: true
        });
    };
    stopEditing = value => {
        this.setState({
            editing: false
        });
        if (value && this.props.onChange) {
            this.props.onChange(value, this.props.name, this.props.index);
        }
    };
    render = () => <div className={'editable-text-field'}>
        <div className={'editable-text-div editable-text-field-label'}>{this.props.title}</div>
        <div className={'editable-text-div editable-text-field-value-container'}>
            {this.state.editing ?
                <EditableTextFieldInput
                    type={this.props.type}
                    value={this.props.value}
                    name={this.props.name}
                    stopEditing={this.stopEditing}
                /> :
                <div className={'text-truncate editable-text-field-value'} onClick={this.startEditing}
                     title={this.props.value}>
                    {this.props.value}
                </div>
            }
        </div>
    </div>;
}
class EditableTextFieldInput extends React.Component {
    state = {
        value: this.props.value
    };
    constructor(props) {
        super(props);
        this.setReference = this.setReference.bind(this);
        this.handleInputChange = this.handleInputChange.bind(this);
        this.stopEditing = this.stopEditing.bind(this);
        this.handleMouseDown = this.handleMouseDown.bind(this);
        this.handleKeyPress = this.handleKeyPress.bind(this);
    }
    componentDidMount = () => {
        document.addEventListener('mousedown', this.handleMouseDown);
        document.addEventListener('keypress', this.handleKeyPress);
    };
    componentWillUnmount = () => {
        document.removeEventListener('mousedown', this.handleMouseDown);
        document.removeEventListener('keypress', this.handleKeyPress);
    };
    setReference = reference => this.reference = reference;
    handleInputChange = event => this.setState({
        value: event.target.value
    });
    handleMouseDown = event =>
        this.reference && !this.reference.contains(event.target) ?
            this.stopEditing(false)() : undefined;
    handleKeyPress = event => {
        switch (event.key) {
            case 'Escape':
                this.stopEditing(false)();
                return;
            case 'Enter':
                if (this.props.type !== 'textarea') {
                    this.stopEditing(true)();
                    return;
                }
                break;
            default:
        }
    };
    stopEditing = (save = false) => () =>
        this.props.stopEditing(save ? this.state.value : undefined);
    render = () => <div ref={this.setReference}>
        <InputGroup>
            <Input
                autoFocus
                type={this.props.type}
                value={this.state.value}
                onChange={this.handleInputChange}/>
            <InputGroupAddon addonType={'append'}>
                <Button
                className={'btn-outline-primary'}
                    onClick={this.stopEditing(true)}
                    color={'success'}
                >
                    <IconCheck/>
                </Button>
                <Button
                className={'btn-outline-primary'}
                    onClick={this.stopEditing(false)}
                    color={'danger'}
                >
                    <IconCancel/>
                </Button>
            </InputGroupAddon>
        </InputGroup>
    </div>;
}
export default EditableTextField;
borgbutler-webapp/src/components/general/forms/FormButton.jsx
New file
@@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import {UncontrolledTooltip} from 'reactstrap';
import classNames from 'classnames';
import {revisedRandId} from "../../../utilities/global";
import I18n from "../translation/I18n";
class FormButton extends React.Component {
    _id = this.props.id || this.props.name || revisedRandId();
    render() {
        let tooltip = null;
        const {className, hint, hintKey, hintPlacement, id, bsStyle, ...other} = this.props;
        if (hint || hintKey) {
            tooltip = <UncontrolledTooltip placement={hintPlacement} target={this._id}>
                {hintKey ? <I18n name={hintKey}/> : hint}
            </UncontrolledTooltip>;
        }
        return (
            <React.Fragment>
                <button
                    {...other}
                    id={this._id}
                    className={classNames(`btn btn-${bsStyle}`, className)}
                >
                    {this.props.children}
                </button>
                {tooltip}
            </React.Fragment>
        );
    }
};
FormButton.propTypes = {
    id: PropTypes.string,
    bsStyle: PropTypes.oneOf(['primary', 'outline-primary', null]),
    type: PropTypes.string,
    onClick: PropTypes.func,
    hint: PropTypes.string,
    hintKey: PropTypes.string,
    hintPlacement: PropTypes.oneOf(['right', 'top']),
    disabled: PropTypes.bool,
    children: PropTypes.node
};
FormButton.defaultProps = {
    bsStyle: 'outline-primary',
    type: 'button',
    hintPlacement: 'top',
    disabled: false
};
export {
    FormButton
};
borgbutler-webapp/src/components/general/forms/FormCheckbox.jsx
New file
@@ -0,0 +1,62 @@
import React from 'react';
import PropTypes from 'prop-types';
import {UncontrolledTooltip} from 'reactstrap';
import {revisedRandId} from "../../../utilities/global";
import classNames from 'classnames';
import I18n from "../translation/I18n";
class FormCheckbox extends React.Component {
    _id = this.props.id || revisedRandId();
    render() {
        const {id, className, labelKey, label, hint, hintKey, ...other} = this.props;
        let tooltip = null;
        let hintId = null;
        if (hint || hintKey) {
            hintId = `hint-${this._id}`;
            tooltip =
                <UncontrolledTooltip placement="right" target={hintId}>
                    {hintKey ? <I18n name={hintKey}/> : hint}
                </UncontrolledTooltip>;
        }
        let labelNode = <label
            className={'custom-control-label'}
            htmlFor={this._id}>
            {labelKey ? <I18n name={labelKey}/> : this.props.label}
        </label>;
        return (
            <React.Fragment>
                <div className="custom-control custom-checkbox" id={hintId}>
                    <input type="checkbox"
                           id={this._id}
                           className={classNames('custom-control-input', className)}
                           {...other}
                    />
                    {labelNode}
                </div>
                {tooltip}
            </React.Fragment>
        );
    }
}
FormCheckbox.propTypes = {
    id: PropTypes.string,
    name: PropTypes.string,
    checked: PropTypes.bool,
    onChange: PropTypes.func,
    hint: PropTypes.string,
    label: PropTypes.node,
    labelKey: PropTypes.string
};
FormCheckbox.defaultProps = {
    checked: false,
    onChange: null
};
export {
    FormCheckbox
};
borgbutler-webapp/src/components/general/forms/FormComponents.jsx
New file
@@ -0,0 +1,298 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {FormFeedback, Input, UncontrolledTooltip} from 'reactstrap';
import {FormCheckbox} from "./FormCheckbox";
import {FormButton} from "./FormButton";
import {FormSelect, FormOption} from "./FormSelect";
import {revisedRandId} from "../../../utilities/global";
import I18n from "../translation/I18n";
const FormGroup = (props) => {
    const {className, ...other} = props;
    return (
        <div className={classNames('form-group row', className)}
             {...other}
        >
            {props.children}
        </div>
    );
}
FormGroup.propTypes = {
    children: PropTypes.node,
    className: PropTypes.string
};
FormGroup.defaultProps = {
    children: null
};
const FormRow = (props) => {
    const {className, ...other} = props;
    return (
        <div className={classNames('form-row', className)}
             {...other}
        >
            {props.children}
        </div>
    );
}
FormGroup.propTypes = {
    children: PropTypes.node,
    className: PropTypes.string
};
FormGroup.defaultProps = {
    children: null
};
const FormLabel = (props) => {
    const {className, i18nKey, length, ...other} = props;
    return (
        <label className={classNames(`col-sm-${props.length} col-form-label`, className)}
               {...other}
        >
            {i18nKey ? <I18n name={i18nKey}/> : props.children}
        </label>
    );
}
FormLabel.propTypes = {
    length: PropTypes.number,
    i18nKey: PropTypes.string,
    htmlFor: PropTypes.string
};
FormLabel.defaultProps = {
    length: 2,
    htmlFor: null
};
const FormInput = (props) => {
    let tooltip = null;
    let targetId = props.id || props.name;
    if (props.hint) {
        tooltip = <UncontrolledTooltip placement={props.hintPlacement} target={targetId}>
            {props.hint}
        </UncontrolledTooltip>;
    }
    const {fieldLength, className, hint, hintPlacement, id, ...other} = props;
    return (
        <React.Fragment>
            <Input id={targetId}
                   className={classNames(`col-sm-${props.fieldLength}`, className)}
                   {...other}
            />
            {tooltip}
        </React.Fragment>
    );
}
FormInput.propTypes = {
    id: PropTypes.string,
    name: PropTypes.string,
    hint: PropTypes.string,
    hintPlacement: PropTypes.oneOf(['right', 'top']),
    fieldLength: PropTypes.number,
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    min: PropTypes.number,
    max: PropTypes.number,
    step: PropTypes.number,
    type: PropTypes.string,
    placeholder: PropTypes.string,
    valid: PropTypes.bool,
    invalid: PropTypes.bool,
    className: PropTypes.string
};
FormInput.defaultProps = {
    id: null,
    name: '',
    hint: null,
    hintPlacement: 'top',
    fieldLength: 10,
    value: '',
    min: null,
    max: null,
    step: 1,
    type: 'text',
    placeholder: '',
    valid: null,
    invalid: null
};
const FormField = (props) => {
    const {className, id, validationMessage, ...other} = props;
    return (
        <div id={props.id}
             className={classNames(`col-sm-${props.length}`, className)}
             {...other}
        >
            {props.children}
            {validationMessage ? <FormFeedback>{validationMessage}</FormFeedback> : ''}
            {props.hint ? <small className={'text-muted'}>{props.hint}</small> : ''}
        </div>
    );
}
FormField.propTypes = {
    id: PropTypes.string,
    hint: PropTypes.node,
    length: PropTypes.number,
    validationMessage: PropTypes.string,
    children: PropTypes.node,
    className: PropTypes.string
};
FormField.defaultProps = {
    id: null,
    hint: null,
    length: 10,
    validationMessage: null,
    children: null
};
const FormLabelField = (props) => {
    const forId = props.children.props.id || props.htmlFor || revisedRandId();
    return (
        <FormGroup>
            <FormLabel length={props.labelLength} htmlFor={forId}>
                {props.label}
            </FormLabel>
            <FormField length={props.fieldLength} hint={props.hint} validationMessage={props.validationMessage}>
                {React.cloneElement(props.children, {id: forId})}
            </FormField>
        </FormGroup>
    );
}
FormLabelField.propTypes = {
    id: PropTypes.string,
    htmlFor: PropTypes.string,
    validationMessage: PropTypes.string,
    labelLength: PropTypes.number,
    fieldLength: PropTypes.number,
    label: PropTypes.node,
    hint: PropTypes.string,
    children: PropTypes.node
};
FormLabelField.defaultProps = {
    id: null,
    htmlFor: null,
    validationMessage: null,
    labelLength: 2,
    fieldLength: 10,
    label: '',
    hint: '',
    children: null
};
const FormLabelInputField = (props) => {
    return (
        <FormLabelField
            htmlFor={props.id}
            labelLength={props.labelLength}
            label={props.label}
            hint={props.hint}
            validationState={props.validationState}
        >
            <FormInput
                id={props.id}
                name={props.name}
                type={props.type}
                min={props.min}
                hint={props.hint}
                hintPlacement={props.hintPlacement}
                max={props.max}
                step={props.step}
                value={props.value}
                onChange={props.onChange}
                fieldLength={props.fieldLength}
                placeholder={props.placeholder}
            />
        </FormLabelField>
    );
}
FormLabelInputField.propTypes = {
    id: PropTypes.string,
    label: PropTypes.node,
    labelLength: PropTypes.number,
    fieldLength: PropTypes.number,
    hint: PropTypes.string,
    hintPlacement: PropTypes.oneOf(['right', 'top']),
    validationState: PropTypes.oneOf(['success', 'warning', 'error', null]),
    type: PropTypes.string,
    name: PropTypes.string,
    min: PropTypes.number,
    max: PropTypes.number,
    step: PropTypes.number,
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    onChange: PropTypes.func,
    placeholder: PropTypes.string
};
FormLabelInputField.defaultProps = {
    id: null,
    label: '',
    labelLength: 2,
    fieldLength: 10,
    hint: null,
    hintPlacement: 'top',
    validationState: null,
    type: 'text',
    name: '',
    min: null,
    max: null,
    step: 1,
    value: '',
    onChange: null,
    placeholder: ''
};
const FormFieldset = (props) => {
    return (
        <fieldset id={props.id}
                  className={classNames('form-group', props.className)}
        >
            <legend>{props.text}</legend>
            {props.children}
        </fieldset>
    );
}
FormFieldset.propTypes = {
    id: PropTypes.string,
    text: PropTypes.node,
    children: PropTypes.node,
    className: PropTypes.string
};
FormFieldset.defaultProps = {
    id: null,
    text: '',
    children: null
};
export {
    FormGroup,
    FormLabel,
    FormField,
    FormLabelField,
    FormInput,
    FormSelect,
    FormOption,
    FormCheckbox,
    FormLabelInputField,
    FormFieldset,
    FormButton,
    FormRow
};
borgbutler-webapp/src/components/general/forms/FormSelect.jsx
New file
@@ -0,0 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {UncontrolledTooltip} from 'reactstrap';
import {getTranslation} from '../../../utilities/i18n';
const FormSelect = (props) => {
    let tooltip = null;
    let targetId = props.id || props.name;
    if (props.hint) {
        tooltip = <UncontrolledTooltip placement={props.hintPlacement} target={targetId}>
            {props.hint}
        </UncontrolledTooltip>;
    }
    const {className, hint, hintPlacement, id, ...other} = props;
    return (
        <React.Fragment>
            <select id={targetId}
                    className={classNames('custom-select form-control form-control-sm mr-1', className)}
                    {...other}
            >
                {props.children}
            </select>
            {tooltip}
        </React.Fragment>
    );
}
FormSelect.propTypes = {
    id: PropTypes.string,
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
    name: PropTypes.string,
    onChange: PropTypes.func,
    hint: PropTypes.string,
    hintPlacement: PropTypes.oneOf(['right', 'top']),
    children: PropTypes.node,
    className: PropTypes.string
};
FormSelect.defaultProps = {
    hint: null,
    hintPlacement: 'top',
};
const FormOption = (props) => {
    let label;
    if (props.i18nKey) {
        label = getTranslation(props.i18nKey) || props.value;
    } else {
        label = props.label || props.value;
    }
    return (
        <React.Fragment>
            <option value={props.value}
            >
                {label}
            </option>
        </React.Fragment>
    );
}
FormSelect.propTypes = {
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
    i18nKey: PropTypes.string,
    label: PropTypes.string
};
export {
    FormSelect, FormOption
};
borgbutler-webapp/src/components/general/loading/LoadingOverlay.jsx
New file
@@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './LoadingOverlay.module.css'
import {IconSpinner} from '../IconComponents';
function LoadingOverlay({active}) {
    if (!active) {
        return <React.Fragment />;
    }
    return (
        <div className={styles.loadingOverlay}>
            <div className={styles.content}>
                <IconSpinner />
            </div>
        </div>
    );
}
LoadingOverlay.propTypes = {
    active: PropTypes.bool
};
LoadingOverlay.defaultProps = {
    active: false
};
export default LoadingOverlay;
borgbutler-webapp/src/components/general/loading/LoadingOverlay.module.css
New file
@@ -0,0 +1,18 @@
div.loadingOverlay {
    z-index: 99999;
    display: block;
    width: 100vw;
    height: 100vh;
    position: absolute;
    top: 0;
    left: 0;
    background-color: rgba(11, 11, 11, 0.3);
    -webkit-backdrop-filter: blur(10px);
}
div.loadingOverlay div.content {
    z-index: 999999;
    color: #ff0f03;
    margin-top: 150px;
    text-align: center;
}
borgbutler-webapp/src/components/general/loading/failed/Overlay.jsx
New file
@@ -0,0 +1,15 @@
import React from 'react';
import {Modal, ModalBody, ModalHeader} from 'reactstrap';
function FailedOverlay({title, text, closeModal, active}) {
    return (
        <Modal isOpen={active} toggle={closeModal}>
            <ModalHeader toggle={closeModal}>{title || 'Fetch failed'}</ModalHeader>
            <ModalBody>
                {text}
            </ModalBody>
        </Modal>
    );
}
export default FailedOverlay;
borgbutler-webapp/src/components/general/translation/I18n.jsx
New file
@@ -0,0 +1,20 @@
import PropTypes from 'prop-types';
import {getTranslation} from '../../../utilities/i18n';
import {isDevelopmentMode} from '../../../utilities/global';
function I18n({name, children, params}) {
    return getTranslation(name, params) || (isDevelopmentMode() ? `??? ${children ? children : name} ???` : children);
}
I18n.propTypes = {
    name: PropTypes.string.isRequired,
    children: PropTypes.node,
    params: PropTypes.arrayOf(PropTypes.node)
};
I18n.defaultProps = {
    children: '',
    params: []
};
export default I18n;
borgbutler-webapp/src/components/views/Start.jsx
New file
@@ -0,0 +1,20 @@
import React from 'react';
import {PageHeader} from '../general/BootstrapComponents';
import I18n from "../general/translation/I18n";
class Start extends React.Component {
    render() {
        return (
            <React.Fragment>
                <PageHeader>
                    <I18n name={'startscreen.welcome.title'}/>
                </PageHeader>
                <div className="welcome-intro"><I18n name={'startscreen.welcome.text'}/></div>
                <div className="welcome-enjoy"><I18n name={'startscreen.welcome.enjoy'}/></div>
                <div className="welcome-documentation-link"><a className={'btn btn-link btn-outline-primary'} href={'/docs/index.html'} target="_blank" rel="noopener noreferrer"><I18n name={'startscreen.welcome.documentation'}/></a></div>
            </React.Fragment>
        );
    }
}
export default Start;
borgbutler-webapp/src/components/views/config/ConfigurationAccountTab.jsx
New file
@@ -0,0 +1,135 @@
import React from 'react';
import {UncontrolledTooltip} from 'reactstrap';
import {
    FormLabelField,
    FormSelect, FormOption, FormGroup, FormLabel, FormField
} from "../../general/forms/FormComponents";
import {getRestServiceUrl} from "../../../utilities/global";
import {clearDictionary} from '../../../utilities/i18n';
import I18n from "../../general/translation/I18n";
import ErrorAlertGenericRestFailure from "../../general/ErrorAlertGenericRestFailure";
import Loading from "../../general/Loading";
import {IconRefresh} from "../../general/IconComponents";
class ConfigAccountTab extends React.Component {
    loadConfig = () => {
        this.setState({
            loading: true,
            failed: false
        });
        fetch(getRestServiceUrl('configuration/user'), {
            method: 'GET',
            dataType: 'JSON',
            headers: {
                'Content-Type': 'text/plain; charset=utf-8'
            }
        })
            .then((resp) => {
                return resp.json()
            })
            .then((data) => {
                const {locale, dateFormat, ...user} = data;
                this.setState({
                    loading: false,
                    locale: locale ? locale : '',
                    dateFormat: dateFormat ? dateFormat : '',
                    ...user
                })
            })
            .catch((error) => {
                console.log("error", error);
                this.setState({
                    loading: false,
                    failed: true
                });
            })
    };
    constructor(props) {
        super(props);
        this.state = {
            loading: true,
            failed: false,
            locale: null,
            dateFormat: null
        };
        this.handleTextChange = this.handleTextChange.bind(this);
        this.loadConfig = this.loadConfig.bind(this);
    }
    componentDidMount() {
        this.loadConfig();
    }
    handleTextChange = event => {
        event.preventDefault();
        this.setState({[event.target.name]: event.target.value});
    }
    save() {
        var user = {
            locale: this.state.locale,
            dateFormat: this.state.dateFormat
        };
        return fetch(getRestServiceUrl("configuration/user"), {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(user)
        })
    }
    render() {
        if (this.state.loading) {
            return <Loading/>;
        }
        if (this.state.failed) {
            return <ErrorAlertGenericRestFailure handleClick={this.loadConfig}/>;
        }
        return (
            <form>
                <div id={'clearDictionary'}
                     className={'btn btn-outline-primary refresh-button-right'}
                     onClick={clearDictionary}
                >
                    <IconRefresh/>
                    <UncontrolledTooltip placement={'left'} target={'clearDictionary'}>
                        <I18n name={'configuration.reloadDictionary.hint'}/>
                    </UncontrolledTooltip>
                </div>
                <FormLabelField label={<I18n name={'configuration.application.language'}/>} fieldLength={2}>
                    <FormSelect value={this.state.locale} name={'locale'} onChange={this.handleTextChange}>
                        <FormOption value={''} i18nKey={'configuration.application.language.option.default'}/>
                        <FormOption value={'en'} i18nKey={'language.english'}/>
                        <FormOption value={'de'} i18nKey={'language.german'}/>
                    </FormSelect>
                </FormLabelField>
                <FormGroup>
                    <FormLabel length={2} htmlFor={'dateFormat'}>
                        <I18n name={'configuration.application.dateFormat'}/>
                    </FormLabel>
                    <FormField length={2}>
                        <FormSelect value={this.state.dateFormat} name={'dateFormat'} onChange={this.handleTextChange}>
                            <FormOption value={''} i18nKey={'configuration.application.dateFormat.option.auto'}/>
                            <FormOption value={'dd.MM.yyyy'} label={'16.01.2018'}/>
                            <FormOption value={'d.M.yy'} label={'16.1.18'}/>
                            <FormOption value={'yyyy-MM-dd'} label={'2018-01-16'}/>
                            <FormOption value={'dd/MM/yyyy'} label={'16/01/2018'}/>
                            <FormOption value={'d/M/yy'} label={'16/1/18'}/>
                            <FormOption value={'MM/dd/yyyy'} label={'01/16/2018'}/>
                            <FormOption value={'M/d/yy'} label={'1/16/18'}/>
                        </FormSelect>
                    </FormField>
                </FormGroup>
            </form>
        );
    }
}
export default ConfigAccountTab;
borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx
New file
@@ -0,0 +1,120 @@
import React from 'react';
import {FormGroup, Nav, NavLink, TabContent, TabPane} from 'reactstrap';
import {PageHeader} from '../../general/BootstrapComponents';
import {FormButton, FormField} from '../../general/forms/FormComponents';
import {isDevelopmentMode} from "../../../utilities/global";
import {clearDictionary} from '../../../utilities/i18n';
import I18n from "../../general/translation/I18n";
import classNames from "classnames";
import ConfigAccountTab from "./ConfigurationAccountTab";
import ConfigServerTab from "./ConfigurationServerTab";
import LoadingOverlay from '../../general/loading/LoadingOverlay';
class ConfigurationPage
    extends React
        .Component {
    constructor(props) {
        super(props);
        this.serverTabRef = React.createRef();
        this.accountTabRef = React.createRef();
        this.state = {
            activeTab: '1',
            loading: false,
            reload: false
        };
        this.onSave = this.onSave.bind(this);
        this.onCancel = this.onCancel.bind(this);
    }
    toggleTab = tab => () => {
        this.setState({
            activeTab: tab
        })
    };
    setReload = () => {
        this.setState({
            reload: true
        })
    }
    async onSave(event) {
        this.setState({
            loading: true
        })
        const cb1 = this.serverTabRef.current ? this.serverTabRef.current.save() : null;
        const cb2 = this.accountTabRef.current ? this.accountTabRef.current.save() : null;
        if (cb1) await cb1;
        if (cb2) await cb2;
        clearDictionary();
        this.setState({
            loading: false
        })
        this.setReload();
    }
    onCancel() {
        this.setReload();
    }
    render() {
        // https://codepen.io/_arpy/pen/xYoyPW
        if (this.state.reload) {
            window.location.reload();
        }
        let todo = '';
        if (isDevelopmentMode()) {
            todo = <code><h3>ToDo</h3>
                <ul>
                    <li>Do the form validation (server and/or client side) with error fields.</li>
                </ul>
            </code>
        }
        return (
            <React.Fragment>
                <PageHeader><I18n name={'configuration'}/></PageHeader>
                <Nav tabs>
                    <NavLink
                        className={classNames({active: this.state.activeTab === '1'})}
                        onClick={this.toggleTab('1')}
                    >
                        <I18n name={'configuration.account'}/>
                    </NavLink>
                    <NavLink
                        className={classNames({active: this.state.activeTab === '2'})}
                        onClick={this.toggleTab('2')}
                    >
                        <I18n name={'configuration.server'}/>
                    </NavLink>
                </Nav>
                <TabContent activeTab={this.state.activeTab}>
                    <TabPane tabId={'1'}>
                        <ConfigAccountTab ref={this.accountTabRef}/>
                    </TabPane>
                </TabContent>
                <TabContent activeTab={this.state.activeTab}>
                    <TabPane tabId={'2'}>
                        <ConfigServerTab ref={this.serverTabRef}/>
                    </TabPane>
                </TabContent>
                <FormGroup>
                    <FormField length={12}>
                        <FormButton onClick={this.onCancel}
                                    hintKey="configuration.cancel.hint"><I18n name={'common.cancel'}/>
                        </FormButton>
                        <FormButton onClick={this.onSave} bsStyle="primary"
                                    hintKey="configuration.save.hint"><I18n name={'common.save'}/>
                        </FormButton>
                    </FormField>
                </FormGroup>
                {todo}
                <LoadingOverlay active={this.state.loading} />
            </React.Fragment>
        );
    }
}
export default ConfigurationPage;
borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx
New file
@@ -0,0 +1,215 @@
import React from 'react';
import {Button, Collapse} from 'reactstrap';
import DirectoryItemsFieldset from './DirectoryItemsFieldset';
import {
    FormButton,
    FormCheckbox,
    FormLabelField,
    FormLabelInputField,
    FormFieldset
} from "../../general/forms/FormComponents";
import {getRestServiceUrl} from "../../../utilities/global";
import {IconDanger, IconWarning} from '../../general/IconComponents';
import {getTranslation} from "../../../utilities/i18n";
import I18n from "../../general/translation/I18n";
import ErrorAlertGenericRestFailure from '../../general/ErrorAlertGenericRestFailure';
import Loading from "../../general/Loading";
var directoryItems = [];
class ConfigServerTab extends React.Component {
    loadConfig = () => {
        this.setState({
            loading: true,
            failed: false
        });
        fetch(getRestServiceUrl('configuration/config'), {
            method: 'GET',
            dataType: 'JSON',
            headers: {
                'Content-Type': 'text/plain; charset=utf-8'
            }
        })
            .then((resp) => {
                return resp.json()
            })
            .then((data) => {
                const {templatesDirs, ...config} = data;
                directoryItems.splice(0, directoryItems.length);
                let idx = 0;
                if (templatesDirs) {
                    templatesDirs.forEach(function (item) {
                        directoryItems.push({index: idx++, directory: item.directory, recursive: item.recursive});
                    });
                }
                config.directoryItems = directoryItems;
                this.setState({
                    loading: false,
                    ...config
                })
            })
            .catch((error) => {
                console.log("error", error);
                this.setState({
                    loading: false,
                    failed: true
                });
            })
    };
    constructor(props) {
        super(props);
        this.state = {
            loading: true,
            failed: false,
            port: 8042,
            showTestData: true,
            webDevelopmentMode: false,
            directoryItems: [],
            redirect: false,
            expertSettingsOpen: false
        };
        this.handleTextChange = this.handleTextChange.bind(this);
        this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
        this.addDirectoryItem = this.addDirectoryItem.bind(this);
        this.removeDirectoryItem = this.removeDirectoryItem.bind(this);
        this.onResetConfiguration = this.onResetConfiguration.bind(this);
        this.loadConfig = this.loadConfig.bind(this);
    }
    componentDidMount() {
        this.loadConfig();
    }
    handleTextChange = event => {
        event.preventDefault();
        this.setState({[event.target.name]: event.target.value});
    }
    handleCheckboxChange = event => {
        this.setState({[event.target.name]: event.target.checked});
    }
    handleDirectoryChange = (index, newDirectory) => {
        const items = this.state.directoryItems;
        items[index].directory = newDirectory;
        // update state
        this.setState({
            directoryItems,
        });
    }
    handleRecursiveFlagChange = (index, newRecursiveState) => {
        const items = this.state.directoryItems;
        items[index].recursive = newRecursiveState;
        // update state
        this.setState({
            directoryItems,
        });
    }
    save() {
        var config = {
            port: this.state.port,
            showTestData: this.state.showTestData,
            webDevelopmentMode: this.state.webDevelopmentMode,
            templatesDirs: []
        };
        if (this.state.directoryItems) {
            this.state.directoryItems.forEach(function (item) {
                config.templatesDirs.push({directory: item.directory, recursive: item.recursive});
            });
        }
        return fetch(getRestServiceUrl("configuration/config"), {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(config)
        })
    }
    onResetConfiguration() {
        if (window.confirm(getTranslation('configuration.resetAllSettings.question'))) {
            fetch(getRestServiceUrl("configuration/reset?IKnowWhatImDoing=true"), {
                method: "GET",
                dataType: "JSON",
                headers: {
                    "Content-Type": "text/plain; charset=utf-8"
                }
            })
        }
    }
    addDirectoryItem() {
        directoryItems.push({
            index: directoryItems.length + 1,
            directory: "",
            recursive: false
        });
        this.setState({directoryItems: directoryItems});
    }
    removeDirectoryItem(itemIndex) {
        directoryItems.splice(itemIndex, 1);
        this.setState({directoryItems: directoryItems});
    }
    render() {
        if (this.state.loading) {
            return <Loading/>;
        }
        if (this.state.failed) {
            return <ErrorAlertGenericRestFailure handleClick={this.loadConfig} />;
        }
        return (
            <form>
                <FormLabelField label={<I18n name={'configuration.showTestData'}/>} fieldLength={2}>
                    <FormCheckbox checked={this.state.showTestData}
                                  name="showTestData"
                                  onChange={this.handleCheckboxChange}/>
                </FormLabelField>
                <DirectoryItemsFieldset items={this.state.directoryItems} addItem={this.addDirectoryItem}
                                        removeItem={this.removeDirectoryItem}
                                        onDirectoryChange={this.handleDirectoryChange}
                                        onRecursiveFlagChange={this.handleRecursiveFlagChange}/>
                <FormLabelField>
                    <Button className={'btn-outline-primary'}
                            onClick={() => this.setState({expertSettingsOpen: !this.state.expertSettingsOpen})}>
                        <IconWarning/> <I18n name={'configuration.forExpertsOnly'}/>
                    </Button>
                </FormLabelField>
                <Collapse isOpen={this.state.expertSettingsOpen}>
                    <FormFieldset text={<I18n name={'configuration.expertSettings'}/>}>
                        <FormLabelInputField label={'Port'} fieldLength={2} type="number" min={0} max={65535}
                                             step={1}
                                             name={'port'} value={this.state.port}
                                             onChange={this.handleTextChange}
                                             placeholder="Enter port"/>
                        <FormLabelField label={<I18n name={'configuration.webDevelopmentMode'}/>} fieldLength={2}>
                            <FormCheckbox checked={this.state.webDevelopmentMode}
                                          hintKey={'configuration.webDevelopmentMode.hint'}
                                          name="webDevelopmentMode"
                                          onChange={this.handleCheckboxChange}/>
                        </FormLabelField>
                        <FormLabelField>
                            <FormButton id={'resetFactorySettings'} onClick={this.onResetConfiguration}
                                        hintKey={'configuration.resetAllSettings.hint'}> <IconDanger/> <I18n
                                name={'configuration.resetAllSettings'}/>
                            </FormButton>
                        </FormLabelField>
                    </FormFieldset>
                </Collapse>
            </form>
        );
    }
}
export default ConfigServerTab;
borgbutler-webapp/src/components/views/config/UpdatePage.jsx
New file
@@ -0,0 +1,126 @@
import React from 'react';
import {connect} from 'react-redux';
import {PageHeader} from '../../general/BootstrapComponents';
import {getRestServiceUrl} from '../../../utilities/global';
import {FormButton} from '../../general/forms/FormComponents';
import {Table} from 'reactstrap';
import I18n from "../../general/translation/I18n";
class UpdatePage extends React.Component {
    onUpdate = () => {
        fetch(getRestServiceUrl('updates/install'), {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json'
            }
        });
    }
    componentDidMount() {
        fetch(getRestServiceUrl("updates/info"), {
            method: "GET",
            dataType: "JSON",
            headers: {
                "Content-Type": "text/plain; charset=utf-8"
            }
        })
            .then((resp) => {
                return resp.json();
            })
            .then((data) => {
                this.setState({
                    version: data.version,
                    installerUrl: data.installerUrl,
                    fileSize: data.fileSize,
                    filename: data.filename,
                    baseUrl: data.baseUrl,
                    comment: data.comment
                });
            })
            .catch((error) => {
                console.log(error, "Oups, what's happened?")
            })
    }
    render() {
        let content = <I18n name={'update.noUpdateAvailable'}/>;
        if (this.props.updateVersion) {
            let comment = null;
            if (this.state.comment) {
                comment = <tr>
                    <th><I18n name={'common.comment'}/></th>
                    <td>{this.state.comment}</td>
                </tr>;
            }
            content = <React.Fragment>
                <h2><I18n name={'update.newVersion'}/></h2>
                <I18n name={'update.newVersionAvailable'} params={[this.props.updateVersion]}/>
                <br/>
                <I18n name={'update.newVersion.simplyClickButton'} />
                <br/>
                <br/>
                <form>
                    <FormButton
                        onClick={this.onUpdate}
                        hintKey={'update.update.button.hint'}
                    >
                        <I18n name={'common.update'}/>
                    </FormButton>
                </form>
                <br/>
                <I18n name={'update.description.line1'}/>
                <br/>
                <I18n name={'update.description.line2'}/>
                <br/>
                <br/>
                <Table striped bordered hover size={'sm'}>
                    <tbody>
                    <tr>
                        <th><I18n name={'version'}/></th>
                        <td>{this.state.version}</td>
                    </tr>
                    <tr>
                        <th><I18n name={'update.installerUrl'}/></th>
                        <td><a href={this.state.installerUrl} target={'_new'}>{this.state.installerUrl}</a></td>
                    </tr>
                    <tr>
                        <th><I18n name={'common.fileSize'}/></th>
                        <td>{this.state.fileSize}</td>
                    </tr>
                    <tr>
                        <th><I18n name={'common.filename'}/></th>
                        <td>{this.state.filename}</td>
                    </tr>
                    {comment}
                    </tbody>
                </Table>
            </React.Fragment>
        }
        return <React.Fragment>
            <PageHeader><I18n name={'update'}/></PageHeader>
            {content}
        </React.Fragment>;
    }
    constructor(props) {
        super(props);
        this.state = {
            version: null,
            installerUrl: null,
            baseUrl: null,
            fileSize: null,
            filename: null,
            comment: null
        }
    }
}
const mapStateToProps = state => ({
    updateVersion: state.version.updateVersion
});
export default connect(mapStateToProps, {})(UpdatePage);
borgbutler-webapp/src/components/views/develop/RestServices.jsx
New file
@@ -0,0 +1,146 @@
import React from 'react';
import {PageHeader} from '../../general/BootstrapComponents';
import {getRestServiceUrl, getResponseHeaderFilename} from "../../../utilities/global";
import fileDownload from 'js-file-download';
class RestUrlLink extends React.Component {
    render() {
        const service = this.props.service;
        const params = this.props.params;
        var url;
        if (params) {
            if (service === 'files/browse-local-filesystem') {
                url = getRestServiceUrl(service) + '?' + params;
            } else {
                url = getRestServiceUrl(service) + '?prettyPrinter=true&' + params;
            }
        } else {
            url = getRestServiceUrl(service) + '?prettyPrinter=true';
        }
        return (
            <a href={url}>rest/{service}{params ? '?' + params : ''}</a>
        )
    }
}
class RestServices extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            templateDefinitionId: '',
            templatePrimaryKey: '',
        }
        this.onRun = this.onRun.bind(this);
    }
    componentDidMount() {
        fetch(getRestServiceUrl("templates/example-definitions"), {
            method: "GET",
            dataType: "JSON",
            headers: {
                "Content-Type": "text/plain; charset=utf-8",
            }
        })
            .then((resp) => {
                return resp.json()
            })
            .then((data) => {
                    this.setState({
                        templateDefinitionId: data.templateDefinitionId,
                        templatePrimaryKey: data.templatePrimaryKey
                    });
                }
            )
            .catch((error) => {
                    console.log(error, "Oups, what's happened?")
                }
            )
    }
    onRun() {
        let filename;
        fetch(getRestServiceUrl('templates/example-run-data'), {
            method: "GET",
            dataType: "JSON",
            headers: {
                "Content-Type": "text/plain; charset=utf-8"
            }
        })
            .then((resp) => {
                return resp.json()
            })
            .then((data) => {
                fetch(getRestServiceUrl("templates/run"), {
                    method: 'POST',
                    body: JSON.stringify(data)
                })
                    .then(response => {
                        filename = getResponseHeaderFilename(response.headers.get("Content-Disposition"));
                        //this.setState({downloadFilename: filename});
                        return response.blob();
                    })
                    .then(blob => fileDownload(blob, filename));
            })
            .catch((error) => {
                console.log(error, "Oups, what's happened?")
            })
    }
    render() {
        return (
            <React.Fragment>
                <PageHeader>
                    Rest Services
                </PageHeader>
                <h3>Templates</h3>
                <ul>
                    <li><RestUrlLink service='templates/list'/></li>
                    <li><RestUrlLink service='templates/definition-list'/></li>
                    <li><RestUrlLink service={'templates/definition'} params={`id=${this.state.templateDefinitionId}`}/> (by id)</li>
                </ul>
                <h4>How to get and run a template:</h4>
                <ol>
                    <li>Get a list of all templates:<br/>
                        <RestUrlLink service='templates/list'/></li>
                    <li>Get a single template from list or get one by its primary key via rest
                        (primaryKey={this.state.templatePrimaryKey}):<br/>
                        <RestUrlLink service='templates/template'
                                     params={'primaryKey=' + encodeURIComponent(this.state.templatePrimaryKey)}/>
                    </li>
                    <li>You will receive a template including its template definition if assigned.</li>
                    <li>Run template with <a
                        href={getRestServiceUrl('templates/example-run-data') + '?prettyPrinter=true'}>json post parameter</a> for service<br/>
                        <button tabIndex={1} onClick={this.onRun} type="button" className="btn btn-link">rest/templates/run</button>
                    </li>
                </ol>
                <h3>
                    Config
                </h3>
                <ul>
                    <li><RestUrlLink service='configuration/user'/></li>
                    <li><RestUrlLink service='configuration/config'/></li>
                    <li><RestUrlLink service='configuration/config-ui'/> (as a trial for dynamic forms)</li>
                    <li><RestUrlLink service='version'/> Gets the version and build date of the server.</li>
                    <li><RestUrlLink service='updates/info'/> Gets the update version.</li>
                    <li><RestUrlLink service='i18n/list'/> Gets all translations. And keys only:{' '}
                        <RestUrlLink service='i18n/list' params={'keysOnly=true'}/> </li>
                </ul>
                <h3>Browse local filesystem</h3>
                <ul>
                    <li><RestUrlLink service='files/browse-local-filesystem' params='type=dir'/></li>
                    <li><RestUrlLink service='files/browse-local-filesystem' params='type=excel'/></li>
                    <li><RestUrlLink service='files/browse-local-filesystem' params='type=word'/></li>
                    <li><RestUrlLink service='files/browse-local-filesystem' params='type=file'/></li>
                </ul>
                <h3>Logging</h3>
                <ul>
                    <li><RestUrlLink service='logging/query'/> (all, default is info log level as treshold)</li>
                    <li><RestUrlLink service='logging/query' params={'treshold=warn'}/> (only warnings)</li>
                    <li><RestUrlLink service='logging/query' params={'treshold=info&search=server'}/> (search for server)</li>
                </ul>
            </React.Fragment>
        );
    }
}
export default RestServices;
borgbutler-webapp/src/components/views/footer/Footer.jsx
New file
@@ -0,0 +1,25 @@
import React from 'react';
import {Link} from 'react-router-dom';
import './style.css'
import {formatDateTime} from '../../../utilities/global';
import I18n from "../../general/translation/I18n";
function Footer({versionInfo}) {
    if (versionInfo.failed) {
        return (
            <div className={'footer'}>
                <p className={'version'}>Cannot fetch version information.</p>
            </div>
        );
    }
    return <div className={'footer'}>
        <p className={'version'}>
            <I18n name={'version'}/> {versionInfo.version} * <I18n name={'version.buildDate'}/> {formatDateTime(versionInfo.buildDate)}
            {versionInfo.updateVersion ? <span> * <Link to={'/update'}><I18n name={'version.updateAvailable'}/></Link></span> : ''}
        </p>
    </div>;
}
export default Footer;
borgbutler-webapp/src/components/views/footer/style.css
New file
@@ -0,0 +1,12 @@
div.footer {
    border-top: 1px solid #ccc;
    height: 60px;
}
div.footer p.version {
    margin-top: 15px;
    text-align: center;
    color: #aaa;
    font-weight: 300;
    font-size: 12px;
}
borgbutler-webapp/src/components/views/logging/LogEmbeddedPanel.jsx
New file
@@ -0,0 +1,106 @@
import React from 'react';
import {getRestServiceUrl} from '../../../utilities/global';
import LogTable from './LogTable';
import './LogViewer.css';
import PropTypes from 'prop-types';
class LogEmbeddedPanel extends React.Component {
    componentDidMount = () => {
        this.reload();
    };
    handleToggleSortOrder = () => {
        this.setState({
            ascendingOrder: (this.state.ascendingOrder === 'true') ? 'false' : 'true'
        }, () => {
            this.reload()
        });
    };
    handleInputChange = (event) => {
        this.props.changeFilter(event.target.name, event.target.value);
    };
    reload = () => {
        this.setState({
            isFetching: true,
            failed: false,
            logEntries: undefined
        });
        fetch(getRestServiceUrl("logging/query", {
            search: this.state.search,
            treshold: this.props.treshold,
            maxSize: this.props.maxSize,
            ascendingOrder: this.state.ascendingOrder,
            mdcTemplatePk: this.props.mdcTemplatePk,
            mdcTemplateDefinitionPk: this.props.mdcTemplateDefinitionPk
        }), {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json'
            }
        })
            .then(response => response.json())
            .then(json => {
                const logEntries = json.map(logEntry => {
                    return {
                        level: logEntry.level.toLowerCase(),
                        message: logEntry.message,
                        logDate: logEntry.logDate,
                        javaClass: logEntry.javaClass,
                        javaClassSimpleName: logEntry.javaClassSimpleName,
                        lineNumber: logEntry.lineNumber,
                        methodName: logEntry.methodName
                    };
                });
                this.setState({
                    isFetching: false,
                    logEntries
                })
            })
            .catch(() => this.setState({isFetching: false, failed: true}));
    };
    render = () => {
        return <LogTable
            search={''}
            locationFormat={this.props.locationFormat}
            entries={this.state.logEntries}
            ascendingOrder={this.state.ascendingOrder}
            toggleOrder={this.handleToggleSortOrder}
            showStackTrace={this.props.showStackTrace}
        />
    };
    constructor(props) {
        super(props);
        this.state = {
            search: '',
            ascendingOrder: 'false'
        }
    }
}
LogEmbeddedPanel.propTypes = {
    locationFormat: PropTypes.oneOf(['none', 'short', 'normal']),
    showStackTrace: PropTypes.oneOf(['true', 'false']),
    threshold: PropTypes.oneOf(['error', 'warn', 'info', 'debug', 'trace']),
    maxSize: PropTypes.oneOf(['50', '100', '500', '1000', '10000']),
    mdcTemplatePk: PropTypes.string,
    mdcTemplateDefinitionPk: PropTypes.string,
    search: PropTypes.string
};
LogEmbeddedPanel.defaultProps = {
    locationFormat: 'none',
    showStackTrace: 'false',
    treshold: 'info',
    maxSize: '50',
    mdcTemplatePk: null,
    mdcTemplateDefinitionPk: null,
    search: ''
};
export default LogEmbeddedPanel;
borgbutler-webapp/src/components/views/logging/LogEntry.jsx
New file
@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import Highlight from 'react-highlighter';
function LogEntry({entry, search, locationString, showStackTrace}) {
    let message = (showStackTrace === 'true' && entry.stackTrace) ? entry.message + <br/> + entry.stackTrace : entry.message;
    return (
        <tr>
            <td className={'tt'}>{entry.logDate}</td>
            <td className={`tt log-${entry.level}`}><Highlight search={search}>{entry.level}</Highlight></td>
            <td className={'tt'}><Highlight search={search}>{message}</Highlight></td>
            {locationString ? <td className={'tt log-location'}><Highlight search={search}>{locationString}</Highlight></td> : undefined}
        </tr>
    );
}
LogEntry.propTypes = {
    entry: PropTypes.shape({}).isRequired,
    search: PropTypes.string,
    locationString: PropTypes.string,
    showStackTrace: PropTypes.oneOf(['true', 'false'])
};
export default LogEntry;
borgbutler-webapp/src/components/views/logging/LogFilters.jsx
New file
@@ -0,0 +1,91 @@
import React from 'react';
import PropTypes from 'prop-types';
import {FormButton, FormInput, FormLabel, FormSelect, FormOption} from '../../general/forms/FormComponents';
import {IconRefresh} from '../../general/IconComponents';
import I18n from '../../general/translation/I18n';
function LogFilters({loadLog, changeFilter, filters}) {
    return (
        <form
            onSubmit={loadLog}
            className={'form-inline'}
        >
            <FormLabel length={1}>
                Filter:
            </FormLabel>
            <FormSelect
                value={filters.threshold}
                name={'threshold'}
                onChange={changeFilter}
                hint={<I18n name={'logviewer.filter.level.hint'}/>}
            >
                <FormOption value={'error'}/>
                <FormOption value={'warn'}/>
                <FormOption value={'info'}/>
                <FormOption value={'debug'}/>
                <FormOption value={'trace'}/>
            </FormSelect>
            <FormInput
                value={filters.search}
                name={'search'}
                onChange={changeFilter}
                fieldLength={5}
            />
            <FormSelect
                value={filters.locationFormat}
                name={'locationFormat'}
                onChange={changeFilter}
                hint={<I18n name={'logviewer.filter.location.hint'}/>}
            >
                <FormOption value={'none'} i18nKey={'common.none'}/>
                <FormOption value={'short'} i18nKey={'logviewer.filter.location.option.short'}/>
                <FormOption value={'normal'} i18nKey={'logviewer.filter.location.option.normal'}/>
            </FormSelect>
            <FormSelect
                value={filters.showStackTrace}
                name={'showStackTrace'}
                onChange={changeFilter}
                hint={<I18n name={'logviewer.filter.stacktraces.showHide.hint'}/>}
            >
                <FormOption value={'false'} i18nKey={'common.none'}/>
                <FormOption value={'true'} i18nKey={'logviewer.filter.stacktraces'}/>
            </FormSelect>
            <FormSelect
                value={filters.maxSize}
                name={'maxSize'}
                onChange={changeFilter}
                hint={<I18n name={'common.limitsResultSize'} />}
            >
                <FormOption value={'50'} />
                <FormOption value={'100'} />
                <FormOption value={'500'} />
                <FormOption value={'1000'} />
                <FormOption value={'10000'} />
            </FormSelect>
            <FormButton type={'submit'} bsStyle={'primary'}>
                <IconRefresh/>
            </FormButton>
        </form>
    );
}
LogFilters.propTypes = {
    changeFilter: PropTypes.func.isRequired,
    filters: PropTypes.shape({
        threshold: PropTypes.oneOf(['error', 'warn', 'info', 'debug', 'trace']),
        search: PropTypes.string,
        locationFormat: PropTypes.oneOf(['none', 'short', 'normal']),
        showStackTrace: PropTypes.oneOf(['true', 'false']),
        maxSize: PropTypes.oneOf(['50', '100', '500', '1000', '10000']),
        ascendingOrder: PropTypes.oneOf(['true', 'false'])
    }).isRequired,
    loadLog: PropTypes.func.isRequired
};
export default LogFilters;
borgbutler-webapp/src/components/views/logging/LogPage.jsx
New file
@@ -0,0 +1,89 @@
import React from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {PageHeader} from '../../general/BootstrapComponents';
import LogFilters from './LogFilters';
import {changeFilter, requestLogReload} from '../../../actions';
import LogTable from './LogTable';
import I18n from '../../general/translation/I18n';
import './LogViewer.css';
import ErrorAlert from '../../general/ErrorAlert';
class LogPage extends React.Component {
    componentDidMount = () => this.props.loadLog();
    handleToggleSortOrder = () => {
        let filters = this.props.filters;
        this.props.changeFilter('ascendingOrder', filters.ascendingOrder === 'true' ? 'false'  : 'true');
        this.props.loadLog();
    };
    handleInputChange = (event) => {
        this.props.changeFilter(event.target.name, event.target.value);
    };
    render = () => {
        let content;
        if (this.props.loading) {
            content = <i>Loading...</i>
        } else if (this.props.failed) {
            content = <ErrorAlert
                title={'Cannot load Log'}
                description={'Something went wrong during contacting the rest api.'}
                action={{
                    handleClick: this.props.loadLog,
                    title: 'Try again'
                }}
            />;
        } else {
            content = <LogTable
                search={this.props.filters.search}
                locationFormat={this.props.filters.locationFormat}
                entries={this.props.entries}
                ascendingOrder={this.props.filters.ascendingOrder}
                toggleOrder={this.handleToggleSortOrder}
                showStackTrace={this.props.filters.showStackTrace}
            />;
        }
        return (
            <React.Fragment>
                <PageHeader><I18n name={'logviewer'} /></PageHeader>
                <LogFilters
                    filters={this.props.filters}
                    changeFilter={this.handleInputChange}
                    loadLog={(event) => {
                        event.preventDefault();
                        this.props.loadLog();
                    }}
                />
                {content}
            </React.Fragment>
        );
    };
}
LogPage.propTypes = {
    changeFilter: PropTypes.func.isRequired,
    filters: PropTypes.shape({
        threshold: PropTypes.oneOf(['error', 'warn', 'info', 'debug', 'trace']),
        search: PropTypes.string,
        locationFormat: PropTypes.oneOf(['none', 'short', 'normal']),
        showStackTrace: PropTypes.oneOf(['true', 'false']),
        maxSize: PropTypes.oneOf(['50', '100', '500', '1000', '10000']),
        ascendingOrder: PropTypes.oneOf(['true', 'false'])
    }).isRequired,
    loadLog: PropTypes.func.isRequired
};
const mapStateToProps = state => state.log;
const actions = {
    changeFilter,
    loadLog: requestLogReload
};
export default connect(mapStateToProps, actions)(LogPage);
borgbutler-webapp/src/components/views/logging/LogTable.jsx
New file
@@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Table} from 'reactstrap';
import LogEntry from './LogEntry';
import {IconSortDown, IconSortUp} from '../../general/IconComponents';
import I18n from '../../general/translation/I18n';
const getLocationString = (locationFormat, entry) => {
    switch (locationFormat) {
        case 'short':
            return `${entry.javaClassSimpleName}:${entry.methodName}:${entry.lineNumber}`;
        case 'normal':
            return `${entry.javaClass}:${entry.methodName}:${entry.lineNumber}`;
        default:
            return null;
    }
};
function LogTable({locationFormat, showStackTrace, entries, search, ascendingOrder, toggleOrder}) {
    const lowercaseSearch = search.toLowerCase();
    let sort = ascendingOrder === 'true' ? <IconSortUp/> : <IconSortDown/>;
    return (
        <Table striped bordered hover size={'sm'} responsive>
            <thead>
            <tr>
                <th style={{whiteSpace: 'nowrap'}}>
                    <I18n name={'logviewer.timestamp'}>Timestamp</I18n>{' '}
                    <button
                        onClick={toggleOrder}
                        type="button"
                        className="btn btn-outline-primary btn-sm"
                    >
                        {sort}
                    </button>
                </th>
                <th><I18n name={'logviewer.level'}>Level</I18n></th>
                <th><I18n name={'logviewer.message'}>Message</I18n></th>
                {locationFormat !== 'none' ? <th><I18n name={'logviewer.location'}>Location</I18n></th> : null}
            </tr>
            </thead>
            <tbody>
            {entries
                .filter(entry => [entry.message, (showStackTrace === 'true') ? entry.stackTrace : '', getLocationString(locationFormat, entry), entry.level, entry.logDate]
                    .join('|#|').toLowerCase()
                    .indexOf(lowercaseSearch) !== -1)
                .map((entry, index) => <LogEntry
                    entry={entry}
                    search={lowercaseSearch}
                    locationString={getLocationString(locationFormat, entry)}
                    showStackTrace={showStackTrace}
                    key={index}
                />)}
            </tbody>
        </Table>
    );
}
LogTable.propTypes = {
    locationFormat: PropTypes.oneOf(['none', 'short', 'normal']),
    entries: PropTypes.array,
    ascendingOrder: PropTypes.oneOf(['true', 'false']),
    search: PropTypes.string
};
LogTable.defaultProps = {
    locationFormat: 'none',
    ascendingOrder: 'false',
    entries: [],
    search: ''
};
export default LogTable;
borgbutler-webapp/src/components/views/logging/LogViewer.css
New file
@@ -0,0 +1,12 @@
td.tt {
    white-space: pre-wrap;
    font-family: monospace;
}
td.log-warn, td.log-error {
    color: red;
    font-weight: bold;
}
mark.highlight {
    padding: 0px;
    background-color: yellow;
}
borgbutler-webapp/src/containers/WebApp.jsx
New file
@@ -0,0 +1,77 @@
import React from 'react';
import createBrowserHistory from 'history/createBrowserHistory';
import {Route, Router, Switch} from 'react-router';
import {connect} from 'react-redux';
import {Badge} from 'reactstrap';
import Menu from '../components/general/Menu';
import Start from '../components/views/Start';
import ConfigurationPage from '../components/views/config/ConfigurationPage';
import RestServices from '../components/views/develop/RestServices';
import {isDevelopmentMode} from '../utilities/global';
import LogPage from '../components/views/logging/LogPage';
import Footer from '../components/views/footer/Footer';
import {loadVersion} from '../actions';
import {getTranslation} from '../utilities/i18n';
import I18n from "../components/general/translation/I18n";
class WebApp extends React.Component {
    history = createBrowserHistory();
    componentDidMount = () => {
        this.props.loadVersion();
    };
    render() {
        let routes = [
            ['Start', '/', Start],
            [getTranslation('logviewer'), '/logging', LogPage],
            [getTranslation('configuration'), '/config', ConfigurationPage]
        ];
        if (this.props.version.updateVersion) {
            routes.push([getTranslation('update'), '/update', UpdatePage, {
                badge: <Badge color={'danger'}><I18n name={'common.new'}/></Badge>,
                className: 'danger'
            }]);
        }
        if (isDevelopmentMode()) {
            routes.push(['Rest services', '/restServices', RestServices]);
        }
        return (
            <Router history={this.history}>
                <div>
                    <Menu routes={routes}/>
                    <div className={'container main-view'}>
                        <Switch>
                            {
                                routes.map((route, index) => (
                                    <Route
                                        key={index}
                                        path={route[1]}
                                        component={route[2]}
                                        exact
                                    />
                                ))
                            }
                        </Switch>
                    </div>
                    <Footer versionInfo={this.props.version}/>
                </div>
            </Router>
        );
    }
}
const mapStateToProps = state => ({
    version: state.version
});
const actions = {
    loadVersion
};
export default connect(mapStateToProps, actions)(WebApp);
borgbutler-webapp/src/containers/WebApp.test.js
New file
@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import WebApp from './WebApp';
it('renders without crashing', () => {
    const div = document.createElement('div');
    ReactDOM.render(<WebApp/>, div);
    ReactDOM.unmountComponentAtNode(div);
});
borgbutler-webapp/src/css/my-style.css
New file
@@ -0,0 +1,406 @@
body {
    color: #50555f;
}
a:hover {
    color: #47a7eb;
}
a.card-link:hover {
    color: #50555f !important;
}
/* Scaffolding */
.container {
    max-width: 1260px;
    margin-top: 100px;
}
div.main-view {
    min-height: calc(100vh - 140px);
}
/* Typo */
h2 {
    font-weight: bold;
    margin-bottom: 20px;
}
h3 {
    font-weight: bold;
}
h4 {
    font-size: 18px;
    font-weight: bold;
    margin-top: 20px;
    margin-bottom: 20px;
}
h5 {
    margin-top: 30px;
    margin-bottom: 10px;
}
/* Navbar */
.navbar.bg-light {
    background: white !important;
    //box-shadow: 0 0 10px rgba(0,0,0,.1);
    border-bottom: 1px solid rgba(0,0,0,.1);
    height: 60px;
}
.navbar-brand {
    font-weight: bold;
}
.navbar.navbar-light a.nav-link {
    color: #7c8495;
    font-size: 1rem;
    font-weight: bold;
}
.navbar a.nav-link.danger {
    color: #dc3545;
}
.navbar.navbar-light a.nav-link.active {
    color: #47a7eb;
}
div.pageheader {
    font-weight: bold;
    font-size: 1.5rem;
    color: #414650;
    margin-bottom: 40px !important;
}
/* Forms */
.form-inline {
    margin-bottom: 30px;
}
input:focus,
select:focus,
textarea:focus,
button:focus {
    outline: none !important;
    outline-width: 0 !important;
}
[contenteditable="true"]:focus {
    outline: none;
}
.text-truncate.editable-text-field-value:hover {
    color: #47a7eb;
}
.input-group > textarea {
    min-height: 40px;
}
.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before {
    background: #7C8495;
}
input[name="search"]{
    margin-right: 5px;
}
.text-truncate.editable-text-field-value {
    padding: 5px 10px;
    background: #f5f6f8;
    border-radius: 3px;
    min-height: 34px;
}
label.custom-control-label {
    min-height: 10px;
}
/* Buttons */
.btn.btn-outline-primary {
    color: #47a7eb;
    background-color: #fff;
    border-color: #309ce8;
}
.btn.btn-outline-primary:hover {
    color: #fff;
    background-color: #47a7eb;
    border-color: #47a7eb;
}
.input-group-append .btn {
    max-height: 40px;
    height: 40px
}
.form-group label {
    margin-bottom: 0px;
}
.btn + .btn {
    margin-left: 10px;
}
/* Tabs */
.nav.nav-tabs {
    margin-bottom: 25px;
}
.nav-tabs .nav-link,
.nav-tabs .nav-link:hover,
.nav-tabs .nav-link.active {
    position: relative;
    top: 1px;
    border-bottom: none;
}
ul.nav-tabs .nav-link:hover {
    cursor: pointer;
    color: #47a7eb;
}
.tab-pane.active {
    padding: 20px;
}
/* Tables */
.table-responsive > .table-bordered {
    font-size: 13px;
    margin-bottom: 50px;
}
.table .btn-outline-primary.btn-sm {
    margin: 0 10px;
}
.table .btn.btn-link {
    padding-left: 0;
}
.table thead th {
    vertical-align: middle;
}
/* Cards */
.template.card {
    color: #50555F;
    max-width: 390px;
    min-width: 390px;
    margin-top: 10px;
    margin-bottom: 20px;
}
.template.card:hover {
    text-decoration: none;
    transform: scale(1.01);
    transition: 0.1s ease-in-out;
}
.template.card .card-header {
    font-weight: bold;
    color: #50555F;
}
.template.card .card-body {
    padding: 0;
}
.template.card .card-footer {
}
.card.border-secondary {
    border-color: #DEE2E6 !important;
}
code {
    padding: 10px;
    display: inline-block;
    border-radius: 3px;
    margin-top: 50px;
    background: #ef94941a;
}
code h3 {
    font-size: 16px;
    font-weight: bold;
}
div.refresh-button-right {
    cursor: pointer;
    color: #555;
    text-align: right;
    float: right;
}
div.refresh-button-right + * {
    clear: both;
}
div.refresh-button-right:hover {
    color: #337ab7;
}
/* Droparea */
div.drop-area {
    overflow: hidden;
    border: 2px dashed #888;
    border-radius: 5px;
    display: table;
    cursor: pointer;
}
div.drop-area div.background {
    background-color: #F2F3F5;
    width: 100%;
    height: 100%;
    display: table;
}
div.drop-area.onDrag div.background {
    background-color: #eee;
}
div.drop-area div.background:hover {
    background: #DEE2E6;
    cursor: grab;
}
div.drop-area input.file {
    border: 0;
    clip: rect(0, 0, 0, 0);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
}
div.drop-area span.info {
    padding: 15px 30px;
    text-align: center;
    font-size: 16px;
    color: #747474;
    display: table-cell;
    vertical-align: middle;
}
#menuDropZone {
    border: 1px dashed #888;
    width: 34px;
    border-radius: 5px;
    margin-left: 10px;
}
#menuDropZone.drop-area svg.fa-upload {
    width: 0.65em;
}
div.drop-area span.info {
    padding: 5px;
}
.main-view div.drop-area span.info {
    padding: 15px 25px;
}
/* Menu */
.collapse.show.navbar-collapse {
    background: #FFF;
    padding-bottom: 10px;
    width: 100%;
    z-index: 9000;
}
.navbar-toggler {
    margin-top: -17px;
}
.welcome-intro {
}
.welcome-enjoy {
    padding-top: 20px;
    font-weight: bold;
    font-style: italic;
}
.welcome-documentation-link {
    padding-top: 20px;
}
/* @media only screen and (max-width: 1050px) {
    .navbar.navbar-light a.nav-link {
        font-size: 0.8rem;
    }
    .collapse.show.navbar-collapse a {
        font-size: 18px;
    }
} */
/* setup tooltips */
/*
Doesn't work for now.
.tooltip {
    position: relative;
}
.tooltip:before,
.tooltip:after {
    display: block;
    opacity: 0;
    pointer-events: none;
    position: absolute;
}
.tooltip:after {
    border-right: 6px solid transparent;
    border-bottom: 6px solid rgba(0,0,0,.75);
    border-left: 6px solid transparent;
    content: '';
    height: 0;
    top: 20px;
    left: 20px;
    width: 0;
    transform: translate3d(0,6px,0);
    transition: all .1s ease-in-out;
}
.tooltip:before {
    background: rgba(0,0,0,.75);
    border-radius: 2px;
    color: #fff;
    content: attr(data-title);
    font-size: 14px;
    padding: 6px 10px;
    top: 26px;
    white-space: nowrap;
    transform: scale3d(.2,.2,1);
    transition: all .2s ease-in-out;
}
.tooltip:hover:after {
    opacity: 1;
    transform: scale3d(1,1,1);
}
.tooltip:hover:after {
    transition: all .2s .1s ease-in-out;
}
*/
borgbutler-webapp/src/index.js
New file
@@ -0,0 +1,47 @@
import '../node_modules/bootstrap/dist/css/bootstrap.css';
import React from 'react';
import ReactDOM from 'react-dom';
import {applyMiddleware, createStore} from 'redux';
import thunk from 'redux-thunk';
import {Provider} from 'react-redux';
import WebApp from './containers/WebApp';
import reducers from './reducers';
import './css/my-style.css';
import {loadDictionary} from './utilities/i18n';
let storedState = window.localStorage.getItem('state');
if (storedState) {
    storedState = JSON.parse(storedState);
    if (storedState.version) {
        storedState.version.loading = false;
    }
    if (storedState.log) {
        storedState.log.loading = false;
    }
}
const store = createStore(
    reducers,
    storedState || undefined,
    applyMiddleware(thunk)
);
loadDictionary(store.getState().version.version, store.getState().version.language);
store.subscribe(() => {
    window.localStorage.setItem('state', JSON.stringify(store.getState()));
});
ReactDOM.render(
    <Provider store={store}>
        <WebApp/>
    </Provider>,
    document.getElementById('root')
);
borgbutler-webapp/src/reducers/index.js
New file
@@ -0,0 +1,12 @@
import {combineReducers} from 'redux';
import log from './log';
import template from './template';
import version from './version';
const reducers = combineReducers({
    log,
    template,
    version
});
export default reducers;
borgbutler-webapp/src/reducers/log.js
New file
@@ -0,0 +1,52 @@
import {
    LOG_VIEW_CHANGE_FILTER,
    LOG_VIEW_FAILED_RELOAD,
    LOG_VIEW_RELOADED,
    LOG_VIEW_REQUEST_RELOAD
} from '../actions/types';
const initialState = {
    filters: {
        threshold: 'info',
        search: '',
        locationFormat: 'none',
        showStackTrace: 'false',
        maxSize: '100',
        ascendingOrder: 'false'
    },
    loading: false,
    failed: false,
    entries: []
};
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case LOG_VIEW_REQUEST_RELOAD:
            return Object.assign({}, state, {
                loading: true,
                failed: false
            });
        case LOG_VIEW_RELOADED:
            return Object.assign({}, state, {
                loading: false,
                entries: action.payload
            });
        case LOG_VIEW_CHANGE_FILTER:
            return Object.assign({}, state, {
                filters: {
                    ...state.filters,
                    [action.payload.name]: action.payload.value
                }
            });
        case LOG_VIEW_FAILED_RELOAD: {
            return Object.assign({}, state, {
                loading: false,
                failed: true
            });
        }
        default:
            return state;
    }
};
export default reducer;
borgbutler-webapp/src/reducers/version.js
New file
@@ -0,0 +1,44 @@
import {VERSION_RELOAD_FAILED, VERSION_RELOADED, VERSION_REQUEST_RELOAD} from '../actions/types';
import {fetchNewDictionary} from '../utilities/i18n';
const initialState = {
    version: '0.0.0',
    buildDate: 'never',
    language: null,
    loading: false
};
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case VERSION_REQUEST_RELOAD:
            return Object.assign({}, state, {
                loading: true,
                failed: false
            });
        case VERSION_RELOADED:
            if (state.version !== action.payload.version ||
                state.language !== action.payload.language) {
                //console.log("reducers/version.js: state.version=" + state.version + ", payload.version=" + action.payload.version + ", state.lang=" + state.language + ", payload.lang=" + action.payload.language);
                fetchNewDictionary(action.payload.version, action.payload.language);
            }
            return Object.assign({}, state, {
                loading: false,
                failed: false,
                version: action.payload.version,
                language: action.payload.language,
                buildDate: action.payload.buildDate,
                updateVersion: action.payload.updateVersion
            });
        case VERSION_RELOAD_FAILED:
            return Object.assign({}, state, {
                loading: false,
                failed: true
            });
        default:
            return state;
    }
};
export default reducer;
borgbutler-webapp/src/utilities/global.js
New file
@@ -0,0 +1,49 @@
// Later Webpack, Context etc. should be used instead.
export const isDevelopmentMode = () => {
    return process.env.NODE_ENV === 'development';
}
global.testserver = 'http://localhost:8042';
global.restBaseUrl = (isDevelopmentMode() ? global.testserver : '') + '/rest';
// Remove registered server workers from Merlin version <= V0.5 and get rid of caching hell:
if (window.navigator && navigator.serviceWorker) {
    navigator.serviceWorker.getRegistrations()
        .then(function (registrations) {
            for (let registration of registrations) {
                console.log('Found serviceWorker registration.');
                registration.unregister();
            }
        });
}
const createQueryParams = params =>
    Object.keys(params)
        .map(k => `${k}=${encodeURI(params[k])}`)
        .join('&');
export const getRestServiceUrl = (restService, params) => {
    if (params) return `${global.restBaseUrl}/${restService}?${createQueryParams(params)}`;
    return `${global.restBaseUrl}/${restService}`;
}
export const getResponseHeaderFilename = contentDisposition => {
    const matches = /filename[^;=\n]*=(UTF-8(['"]*))?(.*)/.exec(contentDisposition);
    return matches && matches.length >= 3 && matches[3] ? decodeURI(matches[3].replace(/['"]/g, '')) : 'download';
};
export const formatDateTime = (millis) => {
    const options = {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'};
    const date = new Date(millis);
    return date.toLocaleDateString(options) + ' ' + date.toLocaleTimeString(options);
    //return date.toLocaleDateString("de-DE", options);
}
export const revisedRandId = () => Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10);
/* Checks if a given array is definied and is not empty. */
export const arrayNotEmpty = (array) => {
    return array && array.length;
}
borgbutler-webapp/src/utilities/i18n.js
New file
@@ -0,0 +1,73 @@
import {getRestServiceUrl} from './global';
let dictionary;
let version;
let language;
const clearDictionary = () => {
    window.localStorage.removeItem('dictionary');
}
const loadDictionary = (version, language) => {
    const localDictionary = window.localStorage.getItem('dictionary');
    if (localDictionary) {
        const json = JSON.parse(localDictionary);
        if (json.version === version && json.language === language) {
            dictionary = json.dictionary;
            return;
        }
        //console.log("Version=" + version + ", lang="+ language + ", json.version=" + json.version + ", json.language=" + json.language);
    } else {
        //console.log("Version=" + version + ", lang="+ language + ", json=undefined");
    }
    fetchNewDictionary(version, language);
};
const fetchNewDictionary = (currentVersion, currentLanguage) => {
    //e.log(new Date().toISOString() + ": version=" + currentVersion + ", lang=" + currentLanguage);
    fetch(getRestServiceUrl('i18n/list'), {
        method: 'GET',
        headers: {
            'Accept': 'application/json'
        }
    })
        .then(response => {
            if (!response.ok) {
                throw new Error(response.statusText);
            }
            return response.json();
        })
        .then(json => {
            dictionary = json;
            version = currentVersion;
            language = currentLanguage;
            saveDictionary();
        });
};
const saveDictionary = () => window.localStorage.setItem('dictionary', JSON.stringify({
    version, language, dictionary
}));
const getTranslation = (key, params) => {
    if (!dictionary) {
        return '';
    }
    let message = dictionary[key];
    if (message && params) {
        params.forEach((param, index) => {
            message = message.replace(`{${index}}`, param);
        });
    }
    return message;
};
export {clearDictionary, getTranslation, loadDictionary, fetchNewDictionary};
settings.gradle
@@ -10,4 +10,11 @@
rootProject.name = 'borgbackup-butler'
include 'borgbutler-core'
include 'borgbutler-server'
include 'borgbutler-webapp'
//project(':borgbutler-docs').projectDir = "$rootDir/docs" as File
project(':borgbutler-webapp').projectDir = "$rootDir/borgbutler-webapp" as File
//startParameter.excludedTaskNames << ':borgbutler-desktop:distTar'
startParameter.excludedTaskNames << ':borgbutler-main:distTar'