100 files added
2 files modified
| | |
| | | *.iws |
| | | borgbutler-core/build |
| | | borgbutler-core/out |
| | | borgbutler-server/out |
| New file |
| | |
| | | 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 |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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") + "'."); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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 + "/"; |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | 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()); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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(), ", "); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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. |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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() { |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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 |
| New file |
| | |
| | | description = 'borgbutler-webapp' |
| | | |
| | | task npmBuild(type: Exec) { |
| | | workingDir '.' |
| | | |
| | | executable 'sh' |
| | | commandLine 'npm', 'run', 'build' |
| | | } |
| New file |
| | |
| | | { |
| | | "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" |
| | | ] |
| | | } |
| New file |
| | |
| | | <?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> |
| New file |
| | |
| | | /*! |
| | | * 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 */ |
| New file |
| | |
| | | <!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> |
| New file |
| | |
| | | { |
| | | "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" |
| | | } |
| | | ] |
| | | } |
| New file |
| | |
| | | import {changeFilter, requestLogReload} from './log'; |
| | | import {loadVersion} from './version'; |
| | | |
| | | export {changeFilter, requestLogReload}; |
| | | |
| | | export {loadVersion}; |
| New file |
| | |
| | | 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); |
| | | } |
| | | }; |
| New file |
| | |
| | | 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'; |
| New file |
| | |
| | | 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())); |
| | | }; |
| New file |
| | |
| | | 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 |
| | | }; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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 |
| | | }; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | import React from 'react'; |
| | | import I18n from "./translation/I18n"; |
| | | |
| | | const Loading = (props) => <div> |
| | | <i><I18n name={'common.loading'}/></i> |
| | | </div> |
| | | |
| | | export default Loading; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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%; |
| | | } |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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 |
| | | }; |
| New file |
| | |
| | | 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 |
| | | }; |
| New file |
| | |
| | | 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 |
| | | }; |
| New file |
| | |
| | | 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 |
| | | }; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| | | |
| New file |
| | |
| | | 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; |
| | | |
| New file |
| | |
| | | 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; |
| | | |
| New file |
| | |
| | | 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); |
| | | |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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); |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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); |
| New file |
| | |
| | | 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); |
| | | }); |
| New file |
| | |
| | | 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; |
| | | } |
| | | */ |
| New file |
| | |
| | | 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') |
| | | ); |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | // 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; |
| | | } |
| New file |
| | |
| | | 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}; |
| | |
| | | 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' |