diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..3fd9d91 --- /dev/null +++ b/404.html @@ -0,0 +1,3001 @@ + + + + + + + + + + + + + + + + + + + DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/Taskfile.yml b/DPL-CMS/Taskfile.yml new file mode 100644 index 0000000..0f26dee --- /dev/null +++ b/DPL-CMS/Taskfile.yml @@ -0,0 +1,63 @@ +version: "3" + +vars: + PLANTUML_RENDERER_VERSION: 1.2021.5 + DRAWIO_EXPORT_VERSION: 4.1.0 + +tasks: + _mkdir: + cmds: + - mkdir -p diagrams/render-png + - mkdir -p diagrams/render-svg + + clean: + desc: Delete all rendered diagrams + cmds: + - rm -fr diagrams/render-png + - rm -fr diagrams/render-svg + + render: + desc: Render all diagrams + cmds: + - task: render:plantuml + - task: render:drawio + + build:plantuml: + desc: Build the container image we use for rendering plantuml + dir: ../tools/plantuml + cmds: + # We do not publish the image as it is very then wrapper around a download + # of platuml an as such having a published image would just be an extra + # thing to keep track of. + - IMAGE_URL=plantuml TAG=0.0.0 PLANTUML_VERSION={{.PLANTUML_RENDERER_VERSION}} task build + + render:plantuml: + desc: Render svg and png versions plantuml diagrams + deps: [_mkdir, build:plantuml] + cmds: + # PDF is currently not supported: https://plantuml.com/pdf + - | + docker run \ + -v "${PWD}/diagrams/:/checkout" \ + -w "/checkout" \ + plantuml:0.0.0 \ + -verbose -tpng -o render-png *.puml + + - | + docker run \ + -v "${PWD}/diagrams/:/checkout" \ + -w "/checkout" \ + plantuml:0.0.0 \ + -verbose -tsvg -o render-svg *.puml + + render:drawio: + desc: Render svg and png versions drawio diagrams + deps: [_mkdir] + cmds: + - | + docker run \ + -v "${PWD}/diagrams:/data" rlespinasse/drawio-export:{{.DRAWIO_EXPORT_VERSION}} --remove-page-suffix --format png --output render-png --scale 2 + + - | + docker run \ + -v "${PWD}/diagrams:/data" rlespinasse/drawio-export:{{.DRAWIO_EXPORT_VERSION}} --remove-page-suffix --format svg --output render-svg --scale 2 \ No newline at end of file diff --git a/DPL-CMS/api-development/index.html b/DPL-CMS/api-development/index.html new file mode 100644 index 0000000..9a4d2d1 --- /dev/null +++ b/DPL-CMS/api-development/index.html @@ -0,0 +1,3195 @@ + + + + + + + + + + + + + + + + + + + + + + + API Development - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

API Development

+

We use the RESTful Web Services and OpenAPI REST Drupal modules +to expose endpoints from Drupal as an API to be consumed by external parties.

+

Howtos

+

Create a new endpoint

+
    +
  1. Implement a new REST resource plugin by extending + Drupal\rest\Plugin\ResourceBase and annotating it with @RestResource
  2. +
  3. Describe uri_paths, route_parameters and responses in the annotation as + detailed as possible to create a strong specification.
  4. +
  5. Install the REST UI module drush pm-enable restui
  6. +
  7. Enable and configure the new REST resource. It is important to use the + dpl_login_user_token authentication provider for all resources which will + be used by the frontend this will provide a library or user token by default.
  8. +
  9. Inspect the updated OpenAPI specification at /openapi/rest?_format=json to + ensure looks as intended
  10. +
  11. Run task ci:openapi:validate to validate the updated OpenAPI specification
  12. +
  13. Run task ci:openapi:download to download the updated OpenAPI specification
  14. +
  15. Uninstall the REST UI module drush pm-uninstall restui
  16. +
  17. Export the updated configuration drush config-export
  18. +
  19. Commit your changes including the updated configuration and openapi.json
  20. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-001a-configuration-management-OLD/index.html b/DPL-CMS/architecture/adr-001a-configuration-management-OLD/index.html new file mode 100644 index 0000000..a5ac7b9 --- /dev/null +++ b/DPL-CMS/architecture/adr-001a-configuration-management-OLD/index.html @@ -0,0 +1,3374 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Configuration Management - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Architecture Decision Record: Configuration Management

+

Notice - this is outdated, and left here only for historical purposes

+

See the new ADR in adr-001b-configuration-management.md

+

Context

+

Configuration management for DPL CMS is a complex issue. The complexity stems +from different types of DPL CMS sites.

+

There are two approaches to the problem:

+
    +
  1. All configuration is local unless explicitly marked as core configuration
  2. +
  3. All configuration is core unless explicitly marked as local configuration
  4. +
+

A solution to configuration management must live up to the following test:

+
    +
  1. Initialize a local environment to represent a site
  2. +
  3. Import the provided configuration through site installation using + drush site-install --existing-config -y
  4. +
  5. Log in to see that Core configuration is imported. This can be verified if + the site name is set to DPL CMS.
  6. +
  7. Change a Core configuration value e.g. on http://dpl-cms.docker/admin/config/development/performance
  8. +
  9. Run drush config-import -y and see that the change is rolled back and the + configuration value is back to default. This shows that Core configuration + will remain managed by the configuration system.
  10. +
  11. Change a local configuration value like the site name on http://dpl-cms.docker/admin/config/system/site-information
  12. +
  13. Run drush config-import -y to see that no configuration is imported. This + shows that local configuration which can be managed by Editor libraries will + be left unchanged.
  14. +
  15. Enable and configure the Shortcut module and add a new Shortcut set.
  16. +
  17. Run drush config-import -y to see that the module is not disabled and the + configuration remains unchanged. This shows that local configuration in the + form of new modules added by Webmaster libraries will be left unchanged.
  18. +
+

Decision

+

We use the Configuration Ignore module +to manage configuration.

+

The module maintains a list of patterns for configuration which will be ignored +during the configuration import process. This allows us to avoid updating local +configuration.

+

By adding the wildcard * at the top of this list we choose an approach where +all configuration is considered local by default.

+

Core configuration which should not be ignored can then be added to subsequent +lines with the ~ which prefix. On a site these configuration entries will be +updated to match what is in the core configuration.

+

Config Ignore also has the option of ignoring specific values within settings. +This is relevant for settings such as system.site where we consider the site +name local configuration but 404 page paths core configuration.

+

Alternatives considered

+

Deconfig + Partial Imports

+

The Deconfig module allows developers +to mark configuration entries as exempt from import/export. This would allow us +to exempt configuration which can be managed by the library.

+

This does not handle configuration coming from new modules uploaded on webmaster +sites. Since we cannot know which configuration entities such modules will +provide and Deconfig has no concept of wildcards we cannot exempt the +configuration from these modules. Their configuration will be removed again at +deployment.

+

We could use partial imports through drush config-import --partial to not +remove configuration which is not present in the configuration filesystem.

+

We prefer Config Ignore as it provides a single solution to handle the entire +problem space.

+

Config Ignore Auto

+

The Config Ignore Auto module +extends the Config Ignore module. Config Ignore Auto registers configuration +changes and adds them to an ignore list. This way they are not overridden on +future deployments.

+

The module is based on the assumption that if an user has access to a +configuration form they should also be allowed to modify that configuration for +their site.

+

This turns the approach from Config Ignore on its head. All configuration is now +considered core until it is changed on the individual site.

+

We prefer Config Ignore as it only has local configuration which may vary +between sites. With Config Ignore Auto we would have local configuration and +the configuration of Config Ignore Auto.

+

Config Ignore Auto also have special handling of the core.extensions +configuration which manages the set of installed modules. Since webmaster sites +can have additional modules installed we would need workarounds to handle these.

+

Config Split

+

The Config Split module allows +developers to split configurations into multiple groups called settings.

+

This would allow us to map the different types of configuration to different +settings.

+

We have not been able to configure this module in a meaningful way which also +passed the provided test.

+

Consequences

+
    +
  • Core developers will have to explicitly select new configuration to not ignore + during the development process. One can not simply run drush config-export + and have the appropriate configuration not ignored.
  • +
  • Because core.extension is ignored Core developers will have to explicitly + enable and uninstall modules through code as a part of the development + process.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-001b-configuration-management/index.html b/DPL-CMS/architecture/adr-001b-configuration-management/index.html new file mode 100644 index 0000000..7f7c7fc --- /dev/null +++ b/DPL-CMS/architecture/adr-001b-configuration-management/index.html @@ -0,0 +1,3342 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Configuration Management - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Configuration Management

+

Context

+

Configuration management for DPL CMS is a complex issue. The complexity stems +from different types of DPL CMS sites.

+

There are two approaches to the problem:

+
    +
  1. All configuration is local unless explicitly marked as core configuration
  2. +
  3. All configuration is core unless explicitly marked as local configuration
  4. +
+

A solution to configuration management must live up to the following test:

+
    +
  1. Initialize a local environment to represent a site
  2. +
  3. Import the provided configuration through site installation using + drush site-install --existing-config -y
  4. +
  5. Log in to see that Core configuration is imported. This can be verified if + the site name is set to DPL CMS.
  6. +
  7. Change a Core configuration value e.g. on http://dpl-cms.docker/admin/config/development/performance
  8. +
  9. Run drush config-import -y and see that the change is rolled back and the + configuration value is back to default. This shows that Core configuration + will remain managed by the configuration system.
  10. +
  11. Change a local configuration value like the site name on http://dpl-cms.docker/admin/config/system/site-information
  12. +
  13. Run drush config-import -y to see that no configuration is imported. This + shows that local configuration which can be managed by Editor libraries will + be left unchanged.
  14. +
  15. Enable and configure the Shortcut module and add a new Shortcut set.
  16. +
  17. Run drush config-import -y to see that the module is not disabled and the + configuration remains unchanged. This shows that local configuration in the + form of new modules added by Webmaster libraries will be left unchanged.
  18. +
+

Decision

+

We use the +Configuration Ignore module +and the Config Ignore Auto module +to manage configuration.

+

The base module maintains a list of patterns for configuration which will be +ignored during the configuration import process. This allows us to avoid +updating local configuration.

+

Here, we can add some of the settings files that we already know needs to be +ignored and admin-respected. +But in reality, we don't need to do this manually, because of the second module:

+

Config Ignore Auto is only enabled on non-development sites. +It works by treating any settings that are updated (site settings, module +settings etc.) as to be ignored. +These settings will NOT be overriden on next deploy by drush config-import.

+

The consequences of using this setup is

+

1) We need to ignore core.extension.yml, for administrators to manage modules + This means that we need to enable/disable new modules using code. + See dpl_base.install for how to do this, through Drupal update hooks. +2) If a faulty permission has been added, or if a decision has been made to + remove an existing permission, there might be config that we dont want to + ignore, that is ignored on some libraries.

+
This means we'll first have to detect which libraries have overriden config
+
+```bash
+  drush config:get config_ignore_auto.settings ignored_config_entities
+    --format json
+```
+
+and then either decide to override it, or migrate the existing.
+
+

3) A last, and final consequence, is that we need to treat permissions more + strictly that we do now.

+
An example is `adminster site settings` also both allows stuff we want to
+ignore (site name), but also things we don't want to ignore (404 node ID).
+
+

Alternatives considered

+

Deconfig + Partial Imports

+

The Deconfig module allows developers +to mark configuration entries as exempt from import/export. This would allow us +to exempt configuration which can be managed by the library.

+

This does not handle configuration coming from new modules uploaded on webmaster +sites. Since we cannot know which configuration entities such modules will +provide and Deconfig has no concept of wildcards we cannot exempt the +configuration from these modules. Their configuration will be removed again at +deployment.

+

We could use partial imports through drush config-import --partial to not +remove configuration which is not present in the configuration filesystem.

+

We prefer Config Ignore as it provides a single solution to handle the entire +problem space.

+

Config Split

+

The Config Split module allows +developers to split configurations into multiple groups called settings.

+

This would allow us to map the different types of configuration to different +settings.

+

We have not been able to configure this module in a meaningful way which also +passed the provided test.

+

Consequences

+
    +
  • Core developers will have to explicitly select new configuration to not ignore + during the development process. One can not simply run drush config-export + and have the appropriate configuration not ignored.
  • +
  • Because core.extension is ignored Core developers will have to explicitly + enable and uninstall modules through code as a part of the development + process.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-002-user-handling/index.html b/DPL-CMS/architecture/adr-002-user-handling/index.html new file mode 100644 index 0000000..fe5ecfc --- /dev/null +++ b/DPL-CMS/architecture/adr-002-user-handling/index.html @@ -0,0 +1,3239 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: User Handling - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: User Handling

+

Context

+

There are different types of users that are interacticting with the CMS system:

+
    +
  • Patrons that is authenticated by logging into Adgangsplatformen.
  • +
  • Editors and administrators (and similar roles) that are are handling content + and configuration of the site.
  • +
+

We need to be able to handle that both type of users can be authenticated and +authorized in the scope of permissions that are tied to the user type.

+

We had some discussions wether the Adgangsplatform users should be tied to a +Drupal user or not. As we saw it we had two options when a user logs in:

+
    +
  1. Keep session/access token client side in the browser and not creating a + Drupal user.
  2. +
  3. Create a Drupal user and map the user with the external user.
  4. +
+

Decision

+

We ended up with desicion no. 2 mentioned above. So we create a Drupal user upon +login if it is not existing already.

+

We use the OpeOpenID Connect / OAuth client module +to manage patron authentication and authorization. And we have developed a +plugin for the module called: Adgangsplatformen which connects the external +oauth service with dpl-cms.

+

Editors and administrators a.k.a normal Drupal users and does not require +additional handling.

+

Consequences

+
    +
  • By having a Drupal user tied to the external user we can use that context and + make the server side rendering show different content according to the + authenticated user.
  • +
  • Adgangsplatform settings have to be configured in the plugin in order to work.
  • +
+

Future considerations

+

Instead of creating a new user for every single user logging in via +Adgangsplatformen you could consider having just one Drupal user for all the +external users. That would get rid of the UUID -> Drupal user id mapping that +has been implemented as it is now. And it would prevent creation of a lot +of users. The decision depends on if it is necessary to distinguish between the +different users on a server side level.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-003-ddb-react-integration/index.html b/DPL-CMS/architecture/adr-003-ddb-react-integration/index.html new file mode 100644 index 0000000..7e1235b --- /dev/null +++ b/DPL-CMS/architecture/adr-003-ddb-react-integration/index.html @@ -0,0 +1,3180 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: DPL React integration - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: DPL React integration

+

Context

+

The DPL React components needs to be integrated and available for rendering in +Drupal. The components are depending on a library token and an access token +being set in javascript.

+

Decision

+

We decided to download the components with composer and integrate them as Drupal +libraries.

+

As described in adr-002-user-handling +we are setting an access token in the user session when a user has been through +a succesful login at Adgangsplatformen.

+

We decided that the library token is fetched by a cron job on a regular basis +and saved in a KeyValueExpirable store which automatically expires the token +when it is outdated.

+

The library token and the access token are set in javascript on the endpoint: +/dpl-react/user.js. By loading the script asynchronically when mounting the +components i javascript we are able to complete the rendering.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-004-ddb-react-caching/index.html b/DPL-CMS/architecture/adr-004-ddb-react-caching/index.html new file mode 100644 index 0000000..479b5c5 --- /dev/null +++ b/DPL-CMS/architecture/adr-004-ddb-react-caching/index.html @@ -0,0 +1,3202 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Caching of DPL React and other js resources - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Caching of DPL React and other js resources

+

Context

+

The general caching strategy is defined in another document and this focused on +describing the caching strategy of DPL react and other js resources.

+

We need to have a caching strategy that makes sure that:

+
    +
  • The js files defined as Drupal libraries (which DPL react is) and pages that + make use of them are being cached.
  • +
  • The same cache is being flushed upon deploy because that is the moment where + new versions of DPL React can be introduced.
  • +
+

Decision

+

We have created a purger in the Drupal Varnish/Purge setup that is able to purge +everything. The purger is being used in the deploy routine by the command: +drush cache:rebuild-external -y

+

Consequences

+
    +
  • Everything will be invalidated on every deploy. Note: Although we are sending + a PURGE request we found out, by studing the vcl of Lagoon, that the PURGE + request actually is being translated into a BAN on req.url.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-005-api-mocking/index.html b/DPL-CMS/architecture/adr-005-api-mocking/index.html new file mode 100644 index 0000000..5d1e8be --- /dev/null +++ b/DPL-CMS/architecture/adr-005-api-mocking/index.html @@ -0,0 +1,3313 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: API mocking - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: API mocking

+

Context

+

DPL CMS integrates with a range of other business systems through APIs. These +APIs are called both clientside (from Browsers) and serverside (from +Drupal/PHP).

+

Historically these systems have provided setups accessible from automated +testing environments. Two factors make this approach problematic going forward:

+
    +
  1. In the future not all systems are guaranteed to provide such environments + with useful data.
  2. +
  3. Test systems have not been as stable as is necessary for automated testing. + Systems may be down or data updated which cause problems.
  4. +
+

To address these problems and help achieve a goal of a high degree of test +coverage the project needs a way to decouple from these external APIs during +testing.

+

Decision

+

We use WireMock to mock API calls. Wiremock provides +the following feature relevant to the project:

+
    +
  • Wiremock is free open source software which can be deployed in development and + tests environment using Docker
  • +
  • Wiremock can run in HTTP(S) proxy mode. This allows us to run a single + instance and mock requests to all external APIs
  • +
  • We can use the wiremock-php + client library to instrument WireMock from PHP code. We modernized the + behat-wiremock-extension + to instrument with Behat tests which we use for integration testing.
  • +
+

Instrumentation vs. record/replay

+

Software for mocking API requests generally provide two approaches:

+
    +
  • Instrumentation where an API can be used to define which responses will be + returned for what requests programmatically.
  • +
  • Record/replay where requests passing through are persisted (typically to the + filesystem) and can be modified and restored at a later point in time.
  • +
+

Generally record/replay makes it easy to setup a lot of mock data quickly. +However, it can be hard to maintain these records as it is not obvious what part +of the data is important for the test and the relationship between the +individual tests and the corresponding data is hard to determine.

+

Consequently, this project prefers instrumentation.

+

Alternatives considered

+

There are many other tools which provide features similar to Wiremock. These +include:

+
    +
  • Hoverfly: FOSS, Docker image and proxy + support. PHP + clients are less mature and no Behat + integration.
  • +
  • Mountebank: FOSS and Docker image. No proxy support, + PHP client + is less mature and no Behat integration.
  • +
  • MockServer: FOSS, Docker image and proxy + support. No PHP client and no Behat integration.
  • +
  • Mockoon: FOSS and Docker image. Does not provide + instrumentation.
  • +
+

Consequences

+
    +
  • Developers may have to engage in maintenance of the wiremock-php and + behat-wiremock-extension library
  • +
+

Status

+

Instrumentation of Wiremock with PHP is made obsolete with the migration from +Behat to Cypress.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-006-api-specification/index.html b/DPL-CMS/architecture/adr-006-api-specification/index.html new file mode 100644 index 0000000..9fe9a12 --- /dev/null +++ b/DPL-CMS/architecture/adr-006-api-specification/index.html @@ -0,0 +1,3250 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: API specification - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: API specification

+

Context

+

DPL CMS provides HTTP end points which are consumed by the React components. We +want to document these in an established structured format.

+

Documenting endpoints in a established structured format allows us to use tools +to generate client code for these end points. This makes consumption easier and +is a practice which is already used with other +services +in the React components.

+

Currently these end points expose business logic tied to configuration in the +CMS. There might be a future where we also need to expose editorial content +through APIs.

+

Decision

+

We use the RESTful Web Services Drupal module +to expose an API from DPL CMS and document the API using the OpenAPI 2.0/Swagger +2.0 specification as supported by the +OpenAPI and OpenAPI REST +Drupal modules.

+

This is a technically manageable, standards compliant and performant solution +which supports our initial use cases and can be expanded to support future +needs.

+

Alternatives considered

+

There are two other approaches to working with APIs and specifications for +Drupal:

+
    +
  • JSON:API: + Drupals JSON:API module + provides many features over the REST module + when it comes to exposing editorial content (or Drupal entities in general). + However it does not work well with other types of functionality which is what + we need for our initial use cases.
  • +
  • GraphQL: + GraphQL is an approach which does not work well with Drupals HTTP based + caching layer. This is important for endpoints which are called many times + for each client. + Also from version 4.x and beyond the GraphQL Drupal module + provides no easy way for us to expose editorial content at a later point in time.
  • +
+

Consequences

+ + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-007-breadcrumb-and-url-patterns/index.html b/DPL-CMS/architecture/adr-007-breadcrumb-and-url-patterns/index.html new file mode 100644 index 0000000..f2553fc --- /dev/null +++ b/DPL-CMS/architecture/adr-007-breadcrumb-and-url-patterns/index.html @@ -0,0 +1,3238 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Breadcrumb structure & URL patterns - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Breadcrumb structure & URL patterns

+

Context

+

The tagging system we have (tags & categories) considers content as 'islands': +Two peices of content may be tagged with the same, but they do not know about +each other.

+

DDF however needs a way to structure content hierarchies. +After a lot of discussion, we reached the conclusion that this can be +materialized through the breadcrumb - the breadcrumb is basically the frontend +version of the content structure tree that editors will create and manage.

+

Because of this, the breadcrumb is "static" and not "dynamic" - e.g., on some +sites, the breadcrumb is built dynamically, based on the route that the user +takes through the site, but in this case, the whole structure is built by +the editors.

+

However, some content is considered "flat islands" - e.g. articles and events +should not know anything about each other, but still be categorized.

+

Either way, the breadcrumb also defines the URL alias.

+

Decision

+

There are two types of breadcrumbs:

+
    +
  • Category-based
  • +
  • Articles and events can be tagged with categories. These categories may
  • +
  • have a hiarchy, and this tree will be displayed as part of the article
  • +
  • breadcrumb.
  • +
  • Content Structure
  • +
  • A custom taxonomy, managed by webmasters, where they choose "core-content + references". This builds the tree.
  • +
  • When creating non-core pages, there is a field that the editor can choose + where this page "lives" in the structure tree.
  • +
  • Based on this, the breadcrumb will be built.
  • +
+

All of this is managed by dpl_breadcrumb module.

+

Alternatives considered

+

We tried using menus instead of taxonomy, based on experience from another +project, but it caused too much confusion and a general poor admin experience. +More info about +that in the ticket comments:

+

Consequences

+

A functional breadcrumb, that is very hard to replace/migrate if we choose a +different direction.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-007-cypress-functional-testing/index.html b/DPL-CMS/architecture/adr-007-cypress-functional-testing/index.html new file mode 100644 index 0000000..6d2b99a --- /dev/null +++ b/DPL-CMS/architecture/adr-007-cypress-functional-testing/index.html @@ -0,0 +1,3249 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Cypress for functional testing - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Cypress for functional testing

+

Context

+

DPL CMS employs functional testing to ensure the functional integrity of the +project.

+

This is currently implemented using Behat +which allows developers to instrument a browser navigating through different use +cases using Gherkin, a +business readable, domain specific language. Behat is used within the project +based on experience using it from the previous generation of DPL CMS.

+

Several factors have caused us to reevaluate that decision:

+ +

Decision

+

We choose to replace Behat with Cypress for functional testing.

+

Alternatives considered

+

There are other prominent tools which can be used for browser based functional +testing:

+ +

Consequences

+ + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-008-external-system-integration/index.html b/DPL-CMS/architecture/adr-008-external-system-integration/index.html new file mode 100644 index 0000000..cbb9eb9 --- /dev/null +++ b/DPL-CMS/architecture/adr-008-external-system-integration/index.html @@ -0,0 +1,3229 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Integration with external systems - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Integration with external systems

+

Context

+

DPL CMS is only intended to integrate with one external system: +Adgangsplatformen. This integration is necessary to obtain patron and library +tokens needed for authentication with other business systems. All these +integrations should occur in the browser through React components.

+

The purpose of this is to avoid having data passing through the CMS as an +intermediary. This way the CMS avoids storing or transmitting sensitive data. +It may also improve performance.

+

In some situations it may be beneficiary to let the CMS access external systems +to provide a better experience for business users e.g. by displaying options +with understandable names instead of technical ids or validating data before it +reaches end users.

+

Decision

+

We choose to allow CMS to access external systems server-side using PHP. +This must be done on behalf of the library - never the patron.

+

Alternatives considered

+
    +
  • Implementing React components to provide administrative controls in the CMS. + This would increase the complexity of implementing such controls and cause + implementors to not consider improvements to the business user experience.
  • +
+

Consequences

+
    +
  • We allow PHP client code generation for external services. These should not + only include APIs to be used with library tokens. This signals what APIs are + OK to be accessed server-side.
  • +
  • The CMS must only access services using the library token provided by the + dpl_library_token.handler service.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-009-translation-system/index.html b/DPL-CMS/architecture/adr-009-translation-system/index.html new file mode 100644 index 0000000..3376576 --- /dev/null +++ b/DPL-CMS/architecture/adr-009-translation-system/index.html @@ -0,0 +1,3321 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Translation system - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Translation system

+

Context

+

The current translation system for UI strings in DPL CMS is based solely on code +deployment of .po files.

+

However DPL CMS is expected to be deployed in about 100 instances just to cover +the Danish Public Library institutions. Making small changes to the UI texts in +the codebase would require a new deployment for each of the instances.

+

Requiring code changes to update translations also makes it difficult for +non-technical participants to manage the process themselves. They have to find a +suitable tool to edit .po files and then pass the updated files to a +developer.

+

This process could be optimized if:

+
    +
  1. Translations were provided by a central source
  2. +
  3. Translations could be managed directly by non-technical users
  4. +
  5. Distribution of translations is decoupled from deployment
  6. +
+

Decision

+

We keep using GitHub as a central source for translation files.

+

We configure Drupal to consume translations from GitHub. The Drupal translation +system already supports runtime updates and consuming translations from a +remote source.

+

We use POEditor to perform translations. POEditor is a +translation management tool that supports .po files and integrates with +GitHub. To detect new UI strings a GitHub Actions workflow scans the codebase +for new strings and notifies POEditor. Here they can be translated by +non-technical users. POEditor supports committing translations back to GitHub +where they can be consumed by DPL CMS instances.

+

Consequences

+

This approach has a number of benefits apart from addressing the original +issue:

+
    +
  • POEditor is a specialized tool to manage translations. It supports features + such as translation memory, glossaries and machine translation.
  • +
  • POEditor is web-based. Translators avoid having to find and install a suitable + tool to edit .po files.
  • +
  • POEditor is software-as-a-service. We do not need to maintain the translation + interface ourselves.
  • +
  • POEditor is free for open source projects. This means that we can use it + without having to pay for a license.
  • +
  • Code scanning means that new UI strings are automatically detected and + available for translation. We do not have to manually synchronize translation + files or ensure that UI strings are rendered by the system before they can be + translated. This can be complex when working with special cases, error + messages etc.
  • +
  • Translations are stored in version control. Managing state is complex and this + means that we have easy visibility into changes.
  • +
  • Translations are stored on GitHub. We can move away from POEditor at any time + and still have access to all translations.
  • +
  • We reuse existing systems instead of building our own.
  • +
+

A consequence of this approach is that developers have to write code that +supports scanning. This is partly supported by the Drupal Code Standards. To +support contexts developers also have to include these as a part of the t() +function call e.g.

+
// Good
+$this->t('A string to be translated', [], ['context' => 'The context']);
+$this->t('Another string', [], ['context' => 'The context']);
+// Bad
+$c = ['context' => 'The context']
+$this->t('A string to be translated', [], $c);
+$this->t('Another string', [], $c);
+
+

We could consider writing a custom sniff or PHPStan rule to enforce this

+

Potion

+

For covering the functionality of scanning the code we had two potential +projects that could solve the case:

+ +

Both projects can scan the codebase and generate a .po or .pot file with the +translation strings and context.

+

At first it made most sense to go for Potx since it is used by +localize.drupal.org and it has a long history. +But Potx is extracting strings to a .pot file without having the possibility +of filling in the existing translations. So we ended up using Potion which can +fill in the existing strings.

+

A flip side using Potion is that it is not being maintained anymore. But it +seems quite stable and a lot of work has been put into it. We could consider to +back it ourselves.

+

Alternatives considered

+

We considered the following alternatives:

+
    +
  1. Establishing our own localization server. This proved to be very complex. + Available solutions are either technically outdated + or still under heavy development. + None of them have integration with GitHub where our project is located.
  2. +
  3. Using a separate instance of DPL CMS in Lagoon as a central translation hub. + Such an instance would require maintenance and we would have to implement a + method for exposing translations to other instances.
  4. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-010-recurring-events/index.html b/DPL-CMS/architecture/adr-010-recurring-events/index.html new file mode 100644 index 0000000..7292d62 --- /dev/null +++ b/DPL-CMS/architecture/adr-010-recurring-events/index.html @@ -0,0 +1,3533 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Recurring events - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Architecture Decision Record: Recurring events

+

Context

+

Events make up an important part of the overall activities the Danish Public +Libraries. They aim to promote information, education and cultural activity. +Events held at the library have many different formats like book readings, +theater for children, tutoring and exhibitions of art. Some of these events +are singular and some are recurring.

+

We define a recurring event as an event that occurs more than once over the +course of a period of time where the primary different between each occurrence +of the event is the event start and end time.

+

A simple solution to this would be to use a multi-value date range field but +there are a number of factors that makes this more challenging:

+

Functional requirements

+
    +
  • Schedules: Editors would like to create occurrences based on a schedule + e.g. every Tuesday from 15:00 to 17:00 between January 1st and March 31th. + This is simpler than having to set each date and time manually.
  • +
  • Reuse: Editors would like to avoid having to retype information for each + instance if it does not wary.
  • +
  • Exceptions: Editors need to be able to create exceptions. An event might + not occur during a holiday.
  • +
  • Variatons: Occurrences may have variations between them. If attendance + requires a ticket, then each occurrence should have a unique url to buy + these. An occurrence can also be marked as sold out or cancelled. This is + preferable to deleting the instance for the sake of communication to end + users.
  • +
  • Relationships: End users would like to a see the relationship between + occurrences. If the date of one occurrence does not fit their personal + schedules it is nice to see the alternatives.
  • +
  • Instances in lists: End users should be able to see individual + occurrences in lists. If an event occurs every Tuesday and the end user + scrolls down a list of events then the event should be presented on every + Tuesday so the end user can get a clear picture of what is going on that day.
  • +
+

Other qualities

+
    +
  • Editorial user experience: Creating schedules can be complex. Editors + should be able to do this without being confronted with fields that are hard + to understand or seem unnecessary.
  • +
  • Maintenance: If we base a solution on third party code we need to + consider future maintenance.
  • +
+

Decision

+

We have decided to base our solution on the Recurring Events Drupal module.

+

The purpose of the module overlaps with our need in regards to handling +recurring events. The module is based on a construct where a recurring event +consists of an event series entity which has one or more related event +instances. Each instance corresponds to an specific date when the event +occurs.

+

The module solves our requirements the following way:

+

Schedule

+

The module supports creating shedules for events with daily, weekly, monthly, +and annual repetitions. Each frequency can be customized, for example, which +days of the week the weekly event should repeat on (e.g., every Tuesday and +Thursday), which days of the month events should repeat on (e.g., the first +Wednesday, the third Friday).

+

Reuse

+

Event series and instances are fieldable entities. The module relies on the +Field Inheritance Drupal module +which allows data to be set on event series and reuse it on individual +entities.

+

Exceptions

+

Recurring events support exceptions in two ways:

+
    +
  1. Editors can delete individual instances after they have been created,
  2. +
  3. Editors can create periods in schedules where no instances should be created. + Such periods can also be created globally to make them apply to all series. + This can be handy for handling national holidays.
  4. +
+

Variations

+

The Field Inheritance module supports different modes of reuse. By using the +fallback method we can allow editors override values from event series on +individual instances.

+

Relationships

+

Recurring events creates a relationship between an event series and individual +instances. Through this relationsship we can determine what other instances +might be for an individual instance.

+

Instances in lists

+

It is possible to create lists of individual instances of events using Views.

+

Editorial user experience

+

Recurring events uses a lot of vertical screen real estate on form elements +needed to define schedules (recurrence type, date/time, schedule, excluded +dates).

+

The module supports defining event duration (30 minutes, 1 hour, 2 hours). +This is simpler than having to potentially repeat date and time.

+

Maintenance

+

Recurring events lists six maintainers on Drupal.org and is supported by +three companies. Among them is Lullabot, a well known company within the +community.

+

The module has over 1.000 sites reported using the module. The latest +version, 2.0.0-rc16, was recently released on December 1th 2023.

+

The dependency, Field Inheritance, currently requires two patches to +Drupal Core.

+

Consequences

+

By introducing new entity types for event series and instance has some +consequences:

+
    +
  • All work currently done in relation to event nodes have to be migrated to + event series and/or instances.
  • +
  • We cannot use modules which only work with nodes. Experience shows that + such modules have been gradually replaced by modules which work with all + entities. Examples include modules like Entity Clone + and Entity Queue.
  • +
  • We cannot use the Drupal Core Views module to create lists of content which + combine nodes like articles and events. To address this need we can use + Search API which supports + creating indices and from these, views, across entity types. We are planning + to use this module anyway.
  • +
+

For future work related to events we have to consider when it is appropriate to +use event series and when to use event instances.

+

To create a consistent data structure for events we have to use recurring +events - even for singular events.

+

We may have to do work to improve the editorial experience. This should +preferably be upstreamed to the Recurring Events module.

+

Going forward we will have to participate in keeping Field Inheritance patches +to Drupal Core updated to match future versions until they are merged.

+

Alternatives considered

+

In the process two alternative Drupal modules were considered:

+
    +
  • Smart date: This was heavily + considered due to good editorial experience and maintenance status. The module + also shows promise in regard to handling opening hours for libraries. For + recurring events the module was eventually discarded due to lacking support + for variations out of the box.
  • +
  • Entity repeat: This was + ruled out due to lack of relationship between event instances, poor editorial + experience and worrying outlook regading maintenance (very small user base, + no official Drupal 10 version)
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-011-configuration-translation-system/index.html b/DPL-CMS/architecture/adr-011-configuration-translation-system/index.html new file mode 100644 index 0000000..614fc4a --- /dev/null +++ b/DPL-CMS/architecture/adr-011-configuration-translation-system/index.html @@ -0,0 +1,3351 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Configuration translation system - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Configuration translation system

+

Context

+

The translation system described in +adr-009-translation-system +handles solely the translations added through Drupals traditional translation handling.

+

But there was a wish for being able to translate configuration related strings +as well. This includes titles, labels, descriptions, +and other elements in the admin area.

+

We needed to find a way to handle translation of that kind as well.

+

Decision

+

We went for a solution where we activated the Configuration Translation +Drupal core module and added the Configuration Translation PO contrib module.

+

And we added a range of custom drush commands to handle the various +configuration translation tasks.

+

Consequences

+

By sticking to the handling of PO files in configuration handling that we are +already using in our general translation handling, +we can keep the current Github workflows with some alterations.

+

Unfortunately handling translations of configuration on local sites +is still as difficult as before. +Translation of configuration texts cannot be found in the standard UI strings +translation list.

+

Alterations to former translation workflow

+

With the config translation PO files added we tried to uncover if POEditor was +able to handle two PO files simultaneously in both import and export context. +It could not.

+

But we still needed, in Drupal, to be able to import two different files: +One for general translations and one for configuration translations.

+

We came up with the idea that we could merge the two files going when importing +into POEditor and split it again when exporting from POEditor.

+

We tried it out and it worked so that was the solution we ended up with.

+

Alternatives considered

+

Using the config_translate module

+

We could activate only the config_translate module and add a danish translations +in config/sync. +But then:

+
    +
  1. We would not be able to use POEditor
  2. +
  3. We would need to translate the string on behalf of the administrators
  4. +
+

A hack

+

We could keep the machine names of the config in English but write the titles, +labels, descriptions in Danish.

+

But that would have the following bad consequences:

+
    +
  1. The administrators would have to find all the texts in various, not obvious, +places in the admin area.
  2. +
  3. It would differ from the general translation routine which is confusing
  4. +
  5. We would not be able to handle multiple languages for the configuration translations
  6. +
+

Extending the Potion module

+

Change the Potion module to be able to scan configuration translations as well.

+

We did not have a clear view of the concept of localizing configuration +translations in the same manner as the Potion module scans the codebase. +It could either be cumbersome to get the two worlds to meet in the same Potion +functionalities or simply incompatible.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-012-api-versioning/index.html b/DPL-CMS/architecture/adr-012-api-versioning/index.html new file mode 100644 index 0000000..d891633 --- /dev/null +++ b/DPL-CMS/architecture/adr-012-api-versioning/index.html @@ -0,0 +1,3326 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: API versioning - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: API versioning

+

Context

+

DPL CMS exposes data and functionality through HTTP endpoints which are +documented by an OpenAPI specification as described in +a previous ADR.

+

Over time this API may need to be updated as the amount of data and +functionality increases and changes. Handling changes is an important aspect +of an API as such changes may affect third parties consuming this API.

+

Decision

+

We use URI versioning of the API exposed +by DPL CMS.

+

This is a simple approach to versioning which works well with the +RESTful Web Services Drupal module +that we use to develop HTTP endpoints with. Through the specification of the +paths provided by the endpoints we can define which version of an API the +endpoint corresponds to.

+

Breaking changes

+

When a breaking change is made the version of the API is increased by one e.g. +from /api/v1/events to /api/v2/events.

+

We consider the following changes breaking:

+
    +
  1. Adding required request parameters to HTTP endpoints
  2. +
  3. Removing functionality of an endpoint (e.g. an HTTP method or request + parameter)
  4. +
  5. Removing an exiting data field in response data
  6. +
  7. Updating the semantics of an existing data field in response data
  8. +
+

The following changes are not considered breaking:

+
    +
  1. Adding optional request parameters
  2. +
  3. Adding additional data fields to existing structures in response data
  4. +
+

The existing version will continue to exist.

+

Alternatives considered

+

Header based versioning

+

Header based versioning is used by +other systems exposing REST APIs +in the infrastructure of the Danish Public Libraries. However we cannot see +this approach working well with the RESTful Web Services Drupal module. +It does not deal with multiple versions of an endpoint which different +specifications.

+

GraphQL

+

Versionless GraphQL APIs are a common practice +Drupal can support GraphQL through a third party module +but using this would require us to reverse our approach to API development and +specification.

+

Consequences

+

Based on this approach we can provide updated versions of our API by leveraging +our existing toolchain.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/architecture/adr-013-javascript-logging/index.html b/DPL-CMS/architecture/adr-013-javascript-logging/index.html new file mode 100644 index 0000000..c762089 --- /dev/null +++ b/DPL-CMS/architecture/adr-013-javascript-logging/index.html @@ -0,0 +1,3265 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: JavaScript logging - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: JavaScript logging

+

Context

+

In the DPL CMS, we integrate React applications within a Drupal system as +outlined in a previous ADR. Effective +JavaScript error logging is crucial for timely detection, diagnosis, and +resolution of issues. It's essential that our logging setup not only captures +client-side errors efficiently but also integrates seamlessly with Drupal's +Watchdog logging system. This allows for logs to be forwarded to Grafana for +real-time monitoring and analysis, enhancing our system's reliability and +performance monitoring capabilities.

+

Decision

+

After evaluating several options, we decided to integrate JSNLog +via the JSNLog Drupal module for +logging JavaScript errors. This integration allows us to capture and log +client-side errors directly from our React components into our server-side +logging infrastructure.

+

Alternatives considered

+
    +
  • +

    JSLog Drupal module:

    +
  • +
  • +

    The module does not have a stable release at the time of writing, which + poses risks regarding reliability and ongoing support.

    +
  • +
  • +

    During testing, it generated excessively large numbers of log entries, + which could overwhelm our logging infrastructure and complicate error analysis.

    +
  • +
  • +

    Custom built solution:

    +
  • +
  • +

    Significant development time and resources required to build, test, and + maintain the module.

    +
  • +
  • +

    Lacks the community support and proven stability found in established + third-party solutions, potentially introducing risks in terms of long-term + reliability and scalability.

    +
  • +
  • +

    Third-party services:

    +
  • +
  • We deliberately dismissed options such as Sentry, Raygun, and similar + third-party services due to our reluctance to introduce additional external + dependencies and complexities.
  • +
  • There are also possibilities for logging in Loki with a third-party library. + We avoided this because of unknown scope and complexities.
  • +
+

Consequences

+
    +
  1. Enhanced Error Detection and Diagnostics:
  2. +
  3. Pros: Improved visibility into client-side errors helps in faster + detection and resolution of issues that impact user experience.
  4. +
  5. Cons: The detailed error logging could potentially lead to larger + volumes of data to manage and analyze, which may require additional resources.
  6. +
  7. Seamless Integration with Existing Systems:
  8. +
  9. Pros: By utilizing a Drupal module that connects JSNLog with Watchdog, + errors logged on the client side are automatically integrated into the + existing Drupal logging framework. This ensures that all system logs are + centralized, simplifying management and analysis.
  10. +
  11. Cons: Dependency on the Drupal module for JSNLog could introduce + complexities, especially if the module is not regularly updated or falls out + of sync with new versions of JSNLog or Drupal.
  12. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/caching/index.html b/DPL-CMS/caching/index.html new file mode 100644 index 0000000..a2df3b6 --- /dev/null +++ b/DPL-CMS/caching/index.html @@ -0,0 +1,3177 @@ + + + + + + + + + + + + + + + + + + + + + + + Caching - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Caching

+

DPL-CMS relies on two levels of caching. Standard Drupal Core caching, and +Varnish as an accelerating HTTP cache.

+

Drupal

+

The Drupal Core cache uses Redis as its storage backend. This takes the load off +of the database-server that is typically shared with other sites.

+

Further more, as we rely on Varnish for all caching of anonymous traffic, the +core Internal Page Cache module has been disabled.

+

Varnish

+

Varnish uses the standard Drupal VCL from lagoon.

+

The site is configured with the Varnish Purge module and configured with a +cache-tags based purger that ensures that changes made to the site, is purged +from Varnish instantly.

+

The configuration follows the Lagoon best practices - reference the +Lagoon documentation on Varnish +for further details.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/code-guidelines/index.html b/DPL-CMS/code-guidelines/index.html new file mode 100644 index 0000000..9449260 --- /dev/null +++ b/DPL-CMS/code-guidelines/index.html @@ -0,0 +1,3784 @@ + + + + + + + + + + + + + + + + + + + + + + + Code guidelines - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Code guidelines

+

The following guidelines describe best practices for developing code for the DPL +CMS project. The guidelines should help achieve:

+
    +
  • A stable, secure and high quality foundation for building and maintaining + library websites
  • +
  • Consistency across multiple developers participating in the project
  • +
  • The best possible conditions for sharing modules between DPL CMS websites
  • +
  • The best possible conditions for the individual DPL CMS website to customize + configuration and appearance
  • +
+

Contributions to the core DPL CMS project will be reviewed by members of the +Core team. These guidelines should inform contributors about what to expect in +such a review. If a review comment cannot be traced back to one of these +guidelines it indicates that the guidelines should be updated to ensure +transparency.

+

Coding standards

+

The project follows the Drupal Coding Standards +and best practices for all parts of the project: PHP, JavaScript and CSS. This +makes the project recognizable for developers with experience from other Drupal +projects. All developers are expected to make themselves familiar with these +standards.

+

The following lists significant areas where the project either intentionally +expands or deviates from the official standards or areas which developers should +be especially aware of.

+

General

+
    +
  • The default language for all code and comments is English.
  • +
+

PHP

+
    +
  • Code must be compatible with all currently available minor and major versions + of PHP from 8.0 and onwards. This is important when trying to ensure smooth + updates going forward. Note that this only applies to custom code.
  • +
  • Code must be compatible with Drupal Best Practices as defined by the + Drupal Coder module
  • +
  • Code must use types + to define function arguments, return values and class properties.
  • +
  • Code must use strict typing.
  • +
+

JavaScript

+
    +
  • All functionality exposed through JavaScript should use the + Drupal JavaScript API + and must be attached to the page using Drupal behaviors.
  • +
  • All classes used for selectors in Javascript must be prefixed with js-. + Example: <div class="gallery js-gallery"> - .gallery must only be used in + CSS, js-gallery must only be used in JS.
  • +
  • Javascript should not affect classes that are not state-classes. State classes + such as is-active, has-child or similar are classes that can be used as + an interlink between JS and CSS.
  • +
+

CSS

+
    +
  • Modules and themes should use SCSS (The project uses PostCSS + and PostCSS-SCSS). The Core system + will ensure that these are compiled to CSS files automatically as a part of + the development process.
  • +
  • Class names should follow the Block-Element-Modifier architecture + (BEM). This rule does not apply to state classes.
  • +
  • Components (blocks) should be isolated from each other. We aim for an + atomic frontend + where components should be able to stand alone. In practice, there will be + times where this is impossible, or where components can technically stand + alone, but will not make sense from a design perspective (e.g. putting a + gallery in a sidebar).
  • +
  • Components should be technically isolated by having 1 component per scss file. + **As a general rule, you can have a file called gallery.scss which contains + .gallery, .gallery__container, .gallery__* and so on. Avoid referencing + other components when possible.
  • +
  • All components/mixins/similar must be documented with a code comment. When you + create a new component.scss, there must be a comment at the top, describing + the purpose of the component.
  • +
  • Avoid using auto-generated Drupal selectors such as .pane-content. Use + the Drupal theme system to write custom HTML and use precise, descriptive + class names. It is better to have several class names on the same element, + rather than reuse the same class name for several components.
  • +
  • All "magic" numbers must be documented. If you need to make something e.g. + 350 px, you preferably need to find the number using calculating from the + context ($layout-width * 0.60) or give it a descriptive variable name + ($side-bar-width // 350px works well with the current $layout-width_)
  • +
  • Avoid using the parent selector (.class &). The use of parent selector + results in complex deeply nested code which is very hard to maintain. There + are times where it makes sense, but for the most part it can and should be + avoided.
  • +
+

Naming

+

Modules

+
    +
  • All modules written specifically for Ding3 must be prefixed with dpl.
  • +
  • The dpl prefix is not required for modules which provide functionality deemed + relevant outside the DPL community and are intended for publication on + Drupal.org.
  • +
+

Files

+

Files provided by modules must be placed in the following folders and have the +extensions defined here.

+
    +
  • General
  • +
  • MODULENAME.*.yml
  • +
  • MODULENAME.module
  • +
  • MODULENAME.install
  • +
  • templates/*.html.twig
  • +
  • Classes, interfaces and traits
  • +
  • src/**/*.php
  • +
  • PHPUnit tests
  • +
  • tests/**/*.php
  • +
  • CSS
  • +
  • If the module does not not use processing: /css/COMPONENTNAME.css
  • +
  • If the module uses preprocessing: /scss/COMPONENTNAME.scss
  • +
  • JavaScript
  • +
  • js/*.js
  • +
  • Images
  • +
  • img/*.(png|jpeg|gif|svg)
  • +
+

Module elements

+

Programmatic elements such as settings, state values and views modules must +comply to a set of common guidelines.

+
    +
  • Machine names should be prefixed with the name of the module that is + responsible for managing the elements.
  • +
  • Administrative titles, human readable names and descriptions should be + relatable to the module name.
  • +
+

As there is no finite set of programmatic elements for a DPL CMS site these +apply to all types unless explicitly specified.

+

Code Structure

+

The project follows the code structure suggested by the +drupal/recommended-project Composer template.

+

Modules, themes etc. must be placed within the corresponding folder in this +repository. If a module developed in relation to this project is of general +purpose to the Drupal community it should be placed on Drupal.org and included +as an external dependency.

+

A module must provide all required code and resources for it to work on its own +or through dependencies. This includes all configuration, theming, CSS, images +and JavaScript libraries.

+

All default configuration required for a module to function should be +implemented using the Drupal configuration system and stored in the version +control with the rest of the project source code.

+

Updating modules

+

If an existing module is expanded with updates to current functionality the +default behavior must be the same as previous versions or as close to this as +possible. This also includes new modules which replaces current modules.

+

If an update does not provide a way to reuse existing content and/or +configuration then the decision on whether to include the update resides with +the business.

+

Altering existing modules

+

Modules which alter or extend functionality provided by other modules should use +appropriate methods for overriding these e.g. by implementing alter hooks or +overriding dependencies.

+

Translations

+

All interface text in modules must be in English. Localization of such texts +must be handled using the Drupal translation API.

+

All interface texts must be provided with a context. This supports separation +between the same text used in different contexts. Unless explicitly stated +otherwise the module machine name should be used as the context.

+

Third party code

+

The project uses package managers to handle code which is developed outside of +the Core project repository. Such code must not be committed to the Core project +repository.

+

The project uses two package manages for this:

+
    +
  • Composer - primarily for managing PHP packages, + Drupal modules and other code libraries which are executed at runtime in the + production environment.
  • +
  • Yarn - primarily for managing code needed to establish + the pipeline for managing frontend assets like linting, preprocessing and + optimization of JavaScript, CSS and images.
  • +
+

When specifying third party package versions the project follows these +guidelines:

+
    +
  • Use the ^ next significant release operator + for packages which follow semantic versioning.
  • +
  • The version specified must be the latest known working and secure version. We + do not want accidental downgrades.
  • +
  • We want to allow easy updates to all working releases within the same major + version.
  • +
  • Packages which are not intended to be executed at runtime in the production + environment should be marked as development dependencies.
  • +
+

Altering third party code

+

The project uses patches rather than forks to modify third party packages. This +makes maintenance of modified packages easier and avoids a collection of forked +repositories within the project.

+
    +
  • Use an appropriate method for the corresponding package manager for managing + the patch.
  • +
  • Patches should be external by default. In rare cases it may be needed to + commit them as a part of the project.
  • +
  • When providing a patch you must document the origin of the patch e.g. through + an url in a commit comment or preferably in the package manager configuration + for the project.
  • +
+

Error handling and logging

+

Code may return null or an empty array for empty results but must throw +exceptions for signalling errors.

+

When throwing an exception the exception must include a meaningful error message +to make debugging easier. When rethrowing an exception then the original +exception must be included to expose the full stack trace.

+

When handling an exception code must either log the exception and continue +execution or (re)throw the exception - not both. This avoids duplicate log +content.

+

Drupal modules must use the Logging API. +When logging data the module must use its name as the logging channel and +an appropriate logging level.

+

Modules integrating with third party services must implement a Drupal setting +for logging requests and responses and provide a way to enable and disable this +at runtime using the administration interface. Sensitive information (such as +passwords, CPR-numbers or the like) must be stripped or obfuscated in the logged +data.

+

Code comments

+

Code comments which describe what an implementation does should only be used +for complex implementations usually consisting of multiple loops, conditional +statements etc.

+

Inline code comments should focus on why an unusual implementation has been +implemented the way it is. This may include references to such things as +business requirements, odd system behavior or browser inconsistencies.

+

Commit messages

+

Commit messages in the version control system help all developers understand the +current state of the code base, how it has evolved and the context of each +change. This is especially important for a project which is expected to have a +long lifetime.

+

Commit messages must follow these guidelines:

+
    +
  1. Each line must not be more than 72 characters long
  2. +
  3. The first line of your commit message (the subject) must contain a short + summary of the change. The subject should be kept around 50 characters long.
  4. +
  5. The subject must be followed by a blank line
  6. +
  7. Subsequent lines (the body) should explain what you have changed and why the + change is necessary. This provides context for other developers who have not + been part of the development process. The larger the change the more + description in the body is expected.
  8. +
  9. If the commit is a result of an issue in a public issue tracker, + platform.dandigbib.dk, then the subject must start with the issue number + followed by a colon (:). If the commit is a result of a private issue tracker + then the issue id must be kept in the commit body.
  10. +
+

When creating a pull request the pull request description should not contain any +information that is not already available in the commit messages.

+

Developers are encouraged to read How to Write a Git Commit Message +by Chris Beams.

+

Tool support

+

The project aims to automate compliance checks as much as possible using static +code analysis tools. This should make it easier for developers to check +contributions before submitting them for review and thus make the review process +easier.

+

The following tools pay a key part here:

+
    +
  1. PHP_Codesniffer with the + following rulesets:
  2. +
  3. Drupal Coding Standards + as defined the Drupal Coder module
  4. +
  5. RequireStrictTypesSniff + as defined by PHP_Codesniffer
  6. +
  7. Eslint and Airbnb JavaScript coding standards + as defined by Drupal Core
  8. +
  9. Prettier as defined by Drupal Core
  10. +
  11. Stylelint with the following rulesets:
  12. +
  13. As defined by Drupal Core
  14. +
  15. BEM as defined by the stylelint-bem project
  16. +
  17. Browsersupport as defined by the + stylelint-no-unsupported-browser-features project
  18. +
  19. PHPStan with the following configuration:
  20. +
  21. Analysis level 8 to support detection of missing types
  22. +
  23. Drupal support as defined by the phpstan-drupal project
  24. +
  25. Detection of deprecated code as defined by the phpstan-deprecation-rules project
  26. +
+

In general all tools must be able to run locally. This allows developers to get +quick feedback on their work.

+

Tools which provide automated fixes are preferred. This reduces the burden of +keeping code compliant for developers.

+

Code which is to be exempt from these standards must be marked accordingly in +the codebase - usually through inline comments (Eslint, +PHP Codesniffer). +This must also include a human readable reasoning. This ensures that deviations +do not affect future analysis and the Core project should always pass through +static analysis.

+

If there are discrepancies between the automated checks and the standards +defined here then developers are encouraged to point this out so the automated +checks or these standards can be updated accordingly.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/config-import/index.html b/DPL-CMS/config-import/index.html new file mode 100644 index 0000000..eb5a384 --- /dev/null +++ b/DPL-CMS/config-import/index.html @@ -0,0 +1,3198 @@ + + + + + + + + + + + + + + + + + + + + + + + Configuration import - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Configuration import

+

Setting up a new site for testing certain scenarios can be repetitive. To avoid +this the project provides a module: DPL Config Import. This module can be used +to import configuration changes into the site and install/uninstall modules in a +single step.

+

The configuration changes are described in a YAML file with configuration entry +keys and values as well as module ids to install or uninstall.

+

How to use

+
    +
  1. Download the example file + that comes with the module.
  2. +
  3. Edit it to set the different configuration values.
  4. +
  5. Upload the file at /admin/config/configuration/import
  6. +
  7. Clear the cache.
  8. +
+

How it is parsed

+

The yaml file has two root elements configuration and modules.

+

A basic file looks like this:

+
configuration:
+  # Add keys for configuration entries to set.
+  # Values will be merged with existing values.
+  system.site:
+    # Configuration values can be set directly
+    slogan: 'Imported by DPL config import'
+    # Nested configuration is also supported
+    page:
+      # All values in nested configuration must have a key. This is required to
+      # support numeric configuration keys.
+      403: '/user/login'
+
+modules:
+  # Add module ids to install or uninstall
+  install:
+    - menu_ui
+  uninstall:
+    - redis
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/configuration-management/index.html b/DPL-CMS/configuration-management/index.html new file mode 100644 index 0000000..2849c63 --- /dev/null +++ b/DPL-CMS/configuration-management/index.html @@ -0,0 +1,3466 @@ + + + + + + + + + + + + + + + + + + + + + + + Configuration Management - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Configuration Management

+

We use the Configuration Ignore Auto module +to manage configuration.

+

In general all configuration is ignored except for configuration which should +explicitly be managed by DPL CMS core.

+

Background

+

Configuration management for DPL CMS is a complex issue. The complexity stems +from the following factors:

+

Site types

+

There are multiple types of DPL CMS sites all using the same code base:

+
    +
  1. Developer (In Danish: Programmør) sites where the library is entirely free + to work with the codebase for DPL CMS as they please for their site
  2. +
  3. Webmaster sites where the library can install and + manage additional modules for their DPL CMS site
  4. +
  5. Editor (In Danish: Redaktør) sites where the library can configure their + site based on predefined configuration options provided by DPL CMS
  6. +
  7. Core sites which are default versions of DPL CMS used for development and + testing purposes
  8. +
+

All these site types must support the following properties:

+
    +
  1. It must be possible for system administrators to deploy new versions of + DPL CMS which may include changes to the site configuration
  2. +
  3. It must be possible for libraries to configure their site based on the + options provided by their type site. This configuration must not be + overridden by new versions of DPL CMS.
  4. +
+

Configuration types

+

This can be split into different types of configuration:

+
    +
  1. Core configuration: This is the configuration for the base installation of + DPL CMS which is shared across all sites. The configuration will be imported + on deployment to support central development of the system.
  2. +
  3. Local configuration: This is the local configuration for the individual + site. The level of configuration depends on the site type but no matter the + type this configuration must not be overridden on deployment of new versions + of DPL CMS.
  4. +
+

Howtos

+

Install a new site from scratch

+
    +
  1. Run drush site-install --existing-config -y
  2. +
+

Add new core configuration

+
    +
  1. Create the relevant configuration through the administration interface
  2. +
  3. Run drush config-export -y
  4. +
  5. Append the key for the configuration to + config_ignore.settings.ignored_config_entities with the ~ prefix
  6. +
  7. Commit the new configuration files and the updated config_ignore.settings + file
  8. +
+

Update existing core configuration

+
    +
  1. Update the relevant configuration through the administration interface
  2. +
  3. Run drush config-export -y
  4. +
  5. Commit the updated configuration files
  6. +
+

NB: The keys for these configuration files should already be in +config_ignore.settings.ignored_config_entities.

+

Add new local configuration

+
    +
  1. Update the relevant configuration through the administration interface
  2. +
  3. Run drush config-export -y
  4. +
  5. Commit the updated configuration files
  6. +
+

Enable a new module

+ +
    +
  1. Add the module to the project code base or as a Composer dependency
  2. +
  3. Create an update hook in the DPL CMS installation profile which enables the + module1. You may want to use dpl_update.install.
  4. +
+
function dpl_update_update_9000() {
+   \Drupal::service('module_installer')->install(['shortcut']);
+}
+
+
    +
  1. Run the update hook locally drush updatedb -y
  2. +
  3. Export configuration drush config-export -y
  4. +
  5. Commit the resulting changes to the site configuration, codebase and/or + Composer files
  6. +
+

Uninstall a existing module

+
    +
  1. Create an update hook in the DPL CMS installation profile which uninstalls + the module1
  2. +
+
function dpl_cms_update_9001() {
+   \Drupal::service('module_installer')->uninstall(['shortcut']);
+}
+
+
    +
  1. Run the update hook locally drush updatedb -y
  2. +
  3. Commit the resulting changes to the site configuration
  4. +
  5. Export configuration drush config-export -y
  6. +
  7. Plan for a future removal of code for the module
  8. +
+ + +

Deploy configuration changes

+
    +
  1. Run drush deploy
  2. +
+

NB: It is important that the official Drupal deployment procedure is followed. +Database updates must be executed before configuration is imported. Otherwise +we risk ending up in a situation where the configuration contains references +to modules which are not enabled.

+
+
+
    +
  1. +

    Creating update hooks for modules is only necessary once we have sites +running in production which will not be reinstalled. Until then it is OK to +enable/uninstall modules as normal and committing changes to core.extensions

    +
  2. +
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/diagrams/add-or-update-translation.puml b/DPL-CMS/diagrams/add-or-update-translation.puml new file mode 100644 index 0000000..e574e9a --- /dev/null +++ b/DPL-CMS/diagrams/add-or-update-translation.puml @@ -0,0 +1,6 @@ +@startuml +Translator -> Poeditor: Translates strings +Translator -> Poeditor: Pushes button to export strings to GitHub +Poeditor -> GitHub: Commits translations to GitHub (develop) +DplCms -> GitHub: By manually requesting or by a cron job translations are imported to DPL CMS. +@enduml diff --git a/DPL-CMS/diagrams/adgangsplatform-login.puml b/DPL-CMS/diagrams/adgangsplatform-login.puml new file mode 100644 index 0000000..0dfa5ce --- /dev/null +++ b/DPL-CMS/diagrams/adgangsplatform-login.puml @@ -0,0 +1,56 @@ +@startuml +actor User as user +participant DPL_CMS as cms +participant OpenidConnect as oc +participant Adgangsplatformen as ap +user -> cms: Click login into Adgangsplatformen +cms -> oc: Get authorization url and return url +oc -> oc: Gets urls and creates oauth state hash + +oc -> user: Tell browser to redirect to authorization url +activate user +note left +The authorization url contains: +* returnurl +* state hash +* agency id +end note +user -> ap: Redirect to external site using the full authorization url +ap -> ap: Internal authentication + +ap -> user: Redirect to the return url +deactivate user +user -> cms: Send Adgangsplatform reponse +cms -> oc: Validate values from the Adgangsplatform reponse + +oc -> ap: Request access token +activate oc +ap -> oc: Returning access token with expire time stamp +oc -> ap: Requesting user info +ap -> oc: Returning user info (UUID) +deactivate oc + +alt First time login - the user is not in Drupal yet + +oc -> cms: Create user +note left +* Random unique email/username set on user. +* The UUID from Adgangsplatformen is encrypted +and used for mapping external user to the Drupal user. +* The Drupal user gets the Drupal role: Patron. +end note + +else Recurrent login - the user exists in Drupal + +oc -> cms: Update user +note left +Nothing is updated on the user +end note + +end + + +oc -> cms: Begin Drupal user session. +oc -> cms: Saving access token in active user session +oc -> user: Redirecting inlogged user to the frontpage +@enduml diff --git a/DPL-CMS/diagrams/adgangsplatform-logout.puml b/DPL-CMS/diagrams/adgangsplatform-logout.puml new file mode 100644 index 0000000..8293592 --- /dev/null +++ b/DPL-CMS/diagrams/adgangsplatform-logout.puml @@ -0,0 +1,30 @@ +@startuml +actor User as user +participant DPL_CMS as cms +participant Adgangsplatformen as ap +user -> cms: Clicks logout +group The user has an access token +cms -> ap: Requests the single logout service at Adgangsplatformen\nThe access token is used in the request +ap -> cms: Response to the cms + +cms -> cms: Logs user out by ending session +note right +The access token is a part of the user session +and gets flushed in the procedure. +end note +cms -> user: Redirects to front page +end + +group The user has no access token +cms -> cms: Logs user out by ending session +cms -> user: Redirects to front page +end +note left +There can be two reasons why the user +does not have an access token: +* The user is a "non-adgangsplatformen" user, eg. an editor. +* Something failed in the access token retrival +and the access token is missing. +end note + +@enduml diff --git a/DPL-CMS/diagrams/render-png/adgangsplatform-login.png b/DPL-CMS/diagrams/render-png/adgangsplatform-login.png new file mode 100644 index 0000000..b045fe3 Binary files /dev/null and b/DPL-CMS/diagrams/render-png/adgangsplatform-login.png differ diff --git a/DPL-CMS/diagrams/render-png/adgangsplatform-logout.png b/DPL-CMS/diagrams/render-png/adgangsplatform-logout.png new file mode 100644 index 0000000..4cece78 Binary files /dev/null and b/DPL-CMS/diagrams/render-png/adgangsplatform-logout.png differ diff --git a/DPL-CMS/diagrams/render-svg/adgangsplatform-login.svg b/DPL-CMS/diagrams/render-svg/adgangsplatform-login.svg new file mode 100644 index 0000000..8eb693b --- /dev/null +++ b/DPL-CMS/diagrams/render-svg/adgangsplatform-login.svg @@ -0,0 +1,66 @@ +UserUserDPL_CMSDPL_CMSOpenidConnectOpenidConnectAdgangsplatformenAdgangsplatformenClick login into AdgangsplatformenGet authorization url and return urlGets urls and creates oauth state hashTell browser to redirect to authorization urlThe authorization url contains:returnurlstate hashagency idRedirect to external site using the full authorization urlInternal authenticationRedirect to the return urlSend Adgangsplatform reponseValidate values from the Adgangsplatform reponseRequest access tokenReturning access token with expire time stampRequesting user infoReturning user info (UUID)alt[First time login - the user is not in Drupal yet]Create userRandom unique email/username set on user.The UUID from Adgangsplatformen is encryptedand used for mapping external user to the Drupal user.The Drupal user gets the Drupal role: Patron.[Recurrent login - the user exists in Drupal]Update userNothing is updated on the userBegin Drupal user session.Saving access token in active user sessionRedirecting inlogged user to the frontpage \ No newline at end of file diff --git a/DPL-CMS/diagrams/render-svg/adgangsplatform-logout.svg b/DPL-CMS/diagrams/render-svg/adgangsplatform-logout.svg new file mode 100644 index 0000000..084ba9b --- /dev/null +++ b/DPL-CMS/diagrams/render-svg/adgangsplatform-logout.svg @@ -0,0 +1,40 @@ +UserUserDPL_CMSDPL_CMSAdgangsplatformenAdgangsplatformenClicks logoutThe user has an access tokenRequests the single logout service at AdgangsplatformenThe access token is used in the requestResponse to the cmsLogs user out by ending sessionThe access token is a part of the user sessionand gets flushed in the procedure.Redirects to front pageThe user has no access tokenThere can be two reasons why the userdoes not have an access token:The user is a "non-adgangsplatformen" user, eg. an editor.Something failed in the access token retrivaland the access token is missing.Logs user out by ending sessionRedirects to front page \ No newline at end of file diff --git a/DPL-CMS/event-field-config/index.html b/DPL-CMS/event-field-config/index.html new file mode 100644 index 0000000..252a889 --- /dev/null +++ b/DPL-CMS/event-field-config/index.html @@ -0,0 +1,3231 @@ + + + + + + + + + + + + + + + + + + + + + + + Event field configuration - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Event field configuration

+

We use the Recurring Events and Field Inheritance modules to manage events. +This creates two fieldable entity types, eventseries and eventinstance, +which we customize to our needs. We try to follow a structured approach when +setting up setup of fields between the two entities.

+

Howto

+

Create a new event field

+
    +
  1. Add a new field on the eventseries entity on /admin/structure/events/series/types/eventseries_type/default/edit/fields
  2. +
  3. Add a new field on the eventinstance entity on /admin/structure/events/instance/types/eventinstance_type/default/edit/fields.
  4. +
  5. Reuse all configuration from the field on eventseries including type, + label, machine name, number of values etc. + Exception: The field on the eventinstance entity must not be + required. Otherwise the Fallback strategy will not work.
  6. +
  7. Add field interitance from eventseries to eventinstanceon /admin/structure/field_inheritance + with the label "Event [field name]" e.g. "Event tags" and the "Fallback" + inheritance strategy.
  8. +
  9. Use eventseries, default and the machine name for the field as the + source.
  10. +
  11. Use eventinstance, default and the machine name for the field as the + destination.
  12. +
+

Render a new field

+
    +
  1. Configure the display of the field for eventseries on /admin/structure/events/series/types/eventseries_type/default/edit/display
  2. +
  3. Configure the display of the field for eventinstance on /admin/structure/events/instance/types/eventinstance_type/default/edit/display
  4. +
  5. Rearrange the fields such that the base field is disabled and the inherited + field is displayed. This is necessary to display the inherited value from + the series if there is no value on the instance but avoid rendering the + value twice if the instance has a value.
  6. +
  7. Configure the display for the inherited field on eventinstance in the same + way as the source field on eventseries.
  8. +
  9. Implement a template for the field in the theme e.g. field--field-tags.html.twig
  10. +
  11. Create a template for the inherited field in the theme. Include the entity + type in the template name to clarify that this is used for event instances + e.g. field--eventinstance--event-tags.html.twig.
  12. +
  13. If the field should work the same across series and instances then include + the series template in the instance template: + {{ include('field--field-tags.html.twig') }}
  14. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/event-integration/index.html b/DPL-CMS/event-integration/index.html new file mode 100644 index 0000000..205db81 --- /dev/null +++ b/DPL-CMS/event-integration/index.html @@ -0,0 +1,3230 @@ + + + + + + + + + + + + + + + + + + + + + + + Event integration - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Event integration

+

Events make up an important part of the overall activities the Danish Public +Libraries. One business aspect of these events is ticketing. Municipalities +in Denmark use different external vendors for handling this responsibility +which includes functionalities such payment, keeping track of availability, +validation, seating etc.

+

On goal for libraries is to keep staff workflows as simple as possible and +avoid duplicate data entry. To achieve this DPL CMS exposes data and +functionality as a part of the public API of the system.

+

Data synchronization

+

The public API for DPL CMS is documented through an OpenAPI 2.0 specification.

+

The following flow diagram represents a suggested approach for synchronizing +event data between DPL CMS and an external system.

+ +
sequenceDiagram
+  Actor EventParticipant
+  Participant DplCms
+  Participant ExternalSystem
+  ExternalSystem ->> DplCms: Retrieve all events
+  activate ExternalSystem
+  activate DplCms
+  DplCms ->> ExternalSystem: List of all publicly available events
+  deactivate DplCms
+  ExternalSystem ->> ExternalSystem: (Optional) Filter out any events that have not been marked as relevant (ticket_manager_relevance)
+  ExternalSystem ->> ExternalSystem: Identify new events by UUID and create them locally
+  ExternalSystem ->> DplCms: Update events with external urls
+  ExternalSystem ->> ExternalSystem: Identify existing events by UUID and update them locally
+  ExternalSystem ->> ExternalSystem: Identify local events with UUID which are<br/>not represented in the list and delete them locally
+  deactivate ExternalSystem
+  Note over DplCms,ExternalSystem: Time passes
+  EventParticipant -->> DplCms: View event
+  EventParticipant -->> DplCms: Purchase ticket
+  DplCms -->> EventParticipant: Refer to external url
+  EventParticipant -->> ExternalSystem: Purchase ticket
+  activate ExternalSystem
+  ExternalSystem -->> EventParticipant: Ticket
+  ExternalSystem ->> DplCms: Update event with state e.g. "sold out"
+  deactivate ExternalSystem
+ + +

Authentication

+

An external system which intends to integrate with events is setup in the same +way as library staff. It is represented by a Drupal user and must be assigned +an appropriate username, password and role by a local administrator for the +library. This information must be communicated to the external system through +other secure means.

+

The external system must authenticate through HTTP basic auth +using this information when updating events.

+

API versioning

+

Please read the related ADR for how +we handle API versioning.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/example-content/index.html b/DPL-CMS/example-content/index.html new file mode 100644 index 0000000..8f679c0 --- /dev/null +++ b/DPL-CMS/example-content/index.html @@ -0,0 +1,3217 @@ + + + + + + + + + + + + + + + + + + + + + + + Example content - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Example content

+

We use the Default Content module +to manage example content. Such content is typically used when setting up +development and testing environments.

+

All actual example content is stored with the DPL Example Content module.

+

Usage of the module in this project is derived from the official documentation.

+

Howtos

+

Add additional default content

+
    +
  1. Create the default content
  2. +
  3. Determine the UUIDs for the entities which should be exported as default + content. The easiest way to do this is to enable the Devel module, view + the entity and go to the Devel tab. + How to find UUID for event using Devl module
  4. +
  5. Add the UUID (s) (and if necessary entity types) to the + dpl_example_content.info.yml file
  6. +
  7. Export the entities by running drush default-content:export-module dpl_example_content
  8. +
  9. Commit the new files under web/modules/custom/dpl_example_content
  10. +
+

Update existing default content

+
    +
  1. Update existing content
  2. +
  3. Export the entities by running drush default-content:export-module dpl_example_content
  4. +
  5. Remove references to and files from UUIDs which are no longer relevant.
  6. +
  7. Commit updated files under web/modules/custom/dpl_example_content
  8. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/images/backup_tab.png b/DPL-CMS/images/backup_tab.png new file mode 100644 index 0000000..af02905 Binary files /dev/null and b/DPL-CMS/images/backup_tab.png differ diff --git a/DPL-CMS/images/devel-uuid.png b/DPL-CMS/images/devel-uuid.png new file mode 100644 index 0000000..2fd7523 Binary files /dev/null and b/DPL-CMS/images/devel-uuid.png differ diff --git a/DPL-CMS/images/retrieve.png b/DPL-CMS/images/retrieve.png new file mode 100644 index 0000000..e4c0137 Binary files /dev/null and b/DPL-CMS/images/retrieve.png differ diff --git a/DPL-CMS/images/virtiofs.png b/DPL-CMS/images/virtiofs.png new file mode 100644 index 0000000..1bc9d4a Binary files /dev/null and b/DPL-CMS/images/virtiofs.png differ diff --git a/DPL-CMS/index.html b/DPL-CMS/index.html new file mode 100644 index 0000000..e5cce0e --- /dev/null +++ b/DPL-CMS/index.html @@ -0,0 +1,3133 @@ + + + + + + + + + + + + + + + + + + + + + + + DPL CMS Documentation - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL CMS Documentation

+

The documentation in this folder describes how to develop DPL CMS.

+

The focus of the documentation is to inform developers of how to develop the +CMS, and give some background behind the various architectural choices.

+

Layout

+

The documentation falls into two categories:

+

The Markdown-files in this +directory document the system as it. Eg. you can read about how to add a new +entry to the core configuration.

+

The ./architecture folder contains our +Architectural Decision Records +that describes the reasoning behind key architecture decisions. Consult these +records if you need background on some part of the CMS, or plan on making any +modifications to the architecture.

+

As for the remaining files and directories

+
    +
  • ./diagrams contains diagram files like draw.io or PlantUML and + rendered diagrams in png/svg format. See the section for details.
  • +
  • ./images this is just plain images used by documentation files.
  • +
  • ./Taskfile.yml a go-task Taskfile, + run task to list available tasks.
  • +
+

Diagrams

+

We strive to keep the diagrams and illustrations used in the documentation as +maintainable as possible. A big part of this is our use of programmatic +diagramming via PlantUML and Open Source based +manual diagramming via diagrams.net (formerly +known as draw.io).

+

When a change has been made to a *.puml or *.drawio file, you should +re-render the diagrams using the command task render and commit the result.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/lagoon-environments/index.html b/DPL-CMS/lagoon-environments/index.html new file mode 100644 index 0000000..e6bcdc8 --- /dev/null +++ b/DPL-CMS/lagoon-environments/index.html @@ -0,0 +1,3436 @@ + + + + + + + + + + + + + + + + + + + + + + + Lagoon environments - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Lagoon environments

+

We use the Lagoon application delivery platform to +host environments for different stages of the DPL CMS project. Our Lagoon +installation is managed by the DPL Platform project.

+

One such type of environment is pull request environments. +These environments are automatically created when a developer creates a pull +request with a change against the project and allows developers and project +owners to test the result before the change is accepted.

+

Howtos

+

Create an environment for a pull request

+
    +
  1. Create a pull request for the change on GitHub. The pull request must be + created from a branch in the same repository as the target branch.
  2. +
  3. Wait for GitHub Actions related to Lagoon deployment to complete. Note: This + deployment process can take a while. Be patient.
  4. +
  5. A link to the deployed environment is available in the section between pull + request activity and Actions
  6. +
  7. The environment is deleted when the pull request is closed
  8. +
+

Access the administration interface for a pull request environment

+

Accessing the administration interface for a pull request environment may be +needed to test certain functionalities. This can be achieved in two ways:

+

Through the Lagoon administration UI

+
    +
  1. Access the administration UI (see below)
  2. +
  3. Go to the environment corresponding to the pull request number
  4. +
  5. Go to the Task section for the environment
  6. +
  7. Select the "Generate login link [drush uli]" task and click "Run task"
  8. +
  9. Refresh the page to see the task in the task list and wait a bit
  10. +
  11. Refresh the page to see the task complete
  12. +
  13. Go to the task page
  14. +
  15. The log output contains a one-time login link which can be used to access + the administration UI
  16. +
+

Through the Lagoon CLI

+
    +
  1. Run task lagoon:drush:uli
  2. +
  3. The log output contains a one-time login link which can be used to access + the administration UI
  4. +
+

Access the Lagoon administration UI

+
    +
  1. Contact administrators of the DPL Platform Lagoon instance to apply for an + user account.
  2. +
  3. Access the URL for the UI of the instance e.g https://ui.lagoon.dplplat01.dpl.reload.dk/
  4. +
  5. Log in with your user account (see above)
  6. +
  7. Go to the dpl-cms project
  8. +
+

Setup the Lagoon CLI

+
    +
  1. Locate information about the Lagoon instance to use in the DPL Platform + documentation
  2. +
  3. Access the URL for the UI of the instance
  4. +
  5. Log in with your user account (see above)
  6. +
  7. Go to the Settings page
  8. +
  9. Add your SSH public key to your account
  10. +
  11. Install the Lagoon CLI
  12. +
  13. Configure the Lagoon CLI to use the instance:
  14. +
+
lagoon config add \
+  --lagoon [instance name e.g. "dpl-platform"] \
+  --hostname [host to connect to with SSH] \
+  --port [SSH port] \
+  --graphql [url to GraphQL endpoint] \
+  --ui [url to UI] \
+
+
    +
  1. Verify the installation:
  2. +
+
lagoon login --lagoon [instance name]
+lagoon whoami --lagoon [instance name]
+
+
    +
  1. Use the DPL Platform as your default Lagoon instance:
  2. +
+
lagoon config default --lagoon [instance name]
+
+

Using cron in pull request environments

+

The .lagoon.yml has an environments section where it is possible to control +various settings. +On root level you specify the environment you want to address (eg.: main). +And on the sub level of that you can define the cron settings. +The cron settings for the main branch looks (in the moment of this writing) +like this:

+
environments:
+  main:
+    cronjobs:
+    - name: drush cron
+      schedule: "M/15 * * * *"
+      command: drush cron
+      service: cli
+
+

If you want to have cron running on a pull request environment, you have to +make a similar block under the environment name of the PR. +Example: In case you would have a PR with the number #135 it would look +like this:

+
environments:
+  pr-135:
+    cronjobs:
+    - name: drush cron
+      schedule: "M/15 * * * *"
+      command: drush cron
+      service: cli
+
+

Workflow with cron in pull request environments

+

This way of making sure cronb is running in the PR environments is +a bit tedious but it follows the way Lagoon is handling it. +A suggested workflow with it could be:

+
    +
  • Create PR with code changes as normally
  • +
  • Write the .lagoon.yml configuration block connected to the current PR #
  • +
  • When the PR has been approved you delete the configuration block again
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/local-development/index.html b/DPL-CMS/local-development/index.html new file mode 100644 index 0000000..ca9fd7a --- /dev/null +++ b/DPL-CMS/local-development/index.html @@ -0,0 +1,3471 @@ + + + + + + + + + + + + + + + + + + + + + + + Local development - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Local development

+

Docker setup

+

Requirements

+

In order to run local development you need:

+
    +
  • go-task
  • +
  • Docker
  • +
  • Preferably support for VIRTUAL_HOST environment variables for Docker + containers. Examples: Dory (OSX) or + nginx-proxy.
  • +
+

MacOS and Docker

+

If you are using MacOS, you can use the standard +"Docker for Mac" +app, however there has been developers that have experienced it acting slow.

+

Alternatively, you can use Orbstack, which is a +direct replacement for Docker for Mac, but is optimized to run much faster.

+

Docker for Mac

+

If you do end up using Docker for Mac, it is +recommended to use VirtioFS on the mounted +volumes in docker-compose, to speed up the containers.

+

OSX preference pane providing access to VirtioFS

+

Howtos

+

Enable XDebug

+

Prerequisites:

+ +

For performance reasons XDebug is disabled by default. It can be enabled +temporarily through a task:

+
    +
  1. Run task dev:enable-xdebug
  2. +
  3. Validate that XDebug is enabled by inspecting http://dpl-cms.docker/admin/reports/status/php. + It should contain extended information about XDebug
  4. +
  5. Debug the application by setting breakpoints, listen for incoming + connections in your IDE and activate XDebug from you client/browser
  6. +
  7. When you are finished, hit enter in the terminal where you enabled XDebug. + This will disable XDebug
  8. +
+

Download database and files from Lagoon

+

Retrieve the latest backup of database and files from Lagoon

+

Prerequisites:

+ +

Run the following command to retrieve the latest backup of database and files +from a Lagoon project:

+
LAGOON_PROJECT=<lagoon-project-name> task lagoon:backup:restore
+
+

Copy a specific database snapshot from Lagoon environment to local setup

+

Prerequisites:

+
    +
  • Login credentials to the Lagoon UI, or an existing database dump
  • +
+

The following describes how to first fetch a database-dump and then import the +dump into a running local environment. Be aware that this only gives you the +database, not any files from the site.

+
    +
  1. To retrieve a database-dump from a running site, consult the + "How do I download a database dump?" + guide in the official Lagoon. Skip this step if you already have a + database-dump.
  2. +
  3. Place the dump in the restore/database directory, be aware + that the directory is only allowed to contain a single .sql file.
  4. +
  5. Start a local environment using task dev:reset
  6. +
  7. Import the database by running task dev:restore:database
  8. +
+

Copy a specific snapshot of files from Lagoon environment to local setup

+

Prerequisites:

+
    +
  • Login credentials to the Lagoon UI, or an existing nginx files dump
  • +
+

The following describes how to first fetch a files backup package +and then replace the files in a local environment.

+

If you need to get new backup files from the remote site:

+ +
    +
  1. Login to the lagoon administration and navigate to the project/environment.
  2. +
  3. Select the backup tab:
  4. +
+

backup_tab image

+
    +
  1. Retrieve the files backup you need:
  2. +
+

retrieve image +4. Due to a UI bug you need to RELOAD the window and then it should be possible + to download the nginx package.

+ + +

Replace files locally:

+
    +
  1. Place the files dump in the files-backup directory, be aware + that the directory is only allowed to contain a single .tar.gz file.
  2. +
  3. Start a local environment using task dev:reset
  4. +
  5. Restore the filesš by running task dev:restore:files
  6. +
+

Get a specific release of dpl-react - without using composer install

+

In a development context it is not very handy only +to be able to get the latest version of the main branch of dpl-react.

+

So a command has been implemented that downloads the specific version +of the assets and overwrites the existing library.

+

You need to specify which branch you need to get the assets from. +The latest HEAD of the given branch is automatically build by Github actions +so you just need to specify the branch you want.

+

It is used like this:

+
BRANCH=[BRANCH_FROM_DPL_REACT_REPOSITORY] task dev:dpl-react:overwrite
+
+

Example:

+
BRANCH=feature/more-releases task dev:dpl-react:overwrite
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/logging/index.html b/DPL-CMS/logging/index.html new file mode 100644 index 0000000..28aafe0 --- /dev/null +++ b/DPL-CMS/logging/index.html @@ -0,0 +1,3259 @@ + + + + + + + + + + + + + + + + + + + + + + + Logging - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Logging

+

We use logging to provide visibility into application behavior and related +user activitiesm to supports effective troubleshooting and debugging. +When an error or issue arises, logs provide the context and history to help +identify what went wrong and where.

+

Architecture

+

We use the logging API provided by Drupal Core +and rely on third party modules to +expose log data to the underlying platform +in a format that is appropriate for consumption by the platform.

+

Logged events

+

We log the following events:

+
    +
  • Significant events occurring during the execution of the content management + system relating to usage and background events. Examples include:
  • +
  • Scheduling of unpublication of events
  • +
  • Renewal of security tokens
  • +
  • Error conditions during the execution of the project codebase. Examples + include:
  • +
  • Inability to retrieve data from external systems
  • +
  • Invalid data provided by external systems
  • +
  • Unexpected state of the local system
  • +
  • Events triggered by Drupal Core and other third party modules used in the + system. Examples include:
  • +
  • User logins
  • +
  • Creation, editing and deletion of content
  • +
  • Execution of background processes
  • +
+

The architecture of the system, where self-service actions carried out by +patrons is handled by JavaScript components running in the browser, +means that searching for materials, management of reservations and updating +patron information cannot be logged by the CMS.

+

Logged data

+

Each logged event contains the following information by default:

+
    +
  • A message specified by the developer in the source code. Examples:
  • +
  • "Session closed for [editorial user]"
  • +
  • "Finished processing scheduled jobs ([time spent] sec, [number of jobs] + total, [number of failures] failed)"
  • +
  • The log severity specified by the developer in the source code.
  • +
  • The date and time the event occurred
  • +
  • Context added by default by Drupal if available for the actor (end user + or external system) when the event occurred:
  • +
  • The associated Drupal user account (anonymized for patrons)
  • +
  • Url accessed
  • +
  • Referring url
  • +
  • IP address
  • +
+

In general sensitive information (such as passwords, CPR-numbers or the like) +must be stripped or obfuscated in the logged data. This is specified by our +coding guidelines. It is the +responsibility of developers to identify such issues during development and +peer review.

+

The architecture of the system severely limits the access to sensitive data +during the execution of the project and thus reduces the general risk.

+

Log severities

+

The system uses the eight log severities specified by PHP-FIG PSR-3 +and Syslog RFC5424 as provided +by the Drupal logging API.

+

Events logged with the severity error or higher are monitored at the platform +level and should be used with this in mind. Note that Drupal will log unchecked +exceptions as this level by default.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/releases/index.html b/DPL-CMS/releases/index.html new file mode 100644 index 0000000..84eb028 --- /dev/null +++ b/DPL-CMS/releases/index.html @@ -0,0 +1,3165 @@ + + + + + + + + + + + + + + + + + + + + + + + Releases - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Releases

+

Building and publishing releases

+

A release of dpl-cms can be build by pushing a tag that matches the following +pattern:

+
# Replace <version> with the version.
+git tag <version>
+
+# Eg.
+git tag 1.2.3
+
+

The actual release is performed by the Publish source Github action which +invokes task source:deploy which in turn uses the tasks source:build and +source:push to build and publish the release.

+

Using the action should be the preferred choice for building and publishing +releases, but should you need to - it is possible to run the task manually +given you have the necessary permissions for pushing the resulting source-image. +Should you only need to produce the image, but not push it the task you can opt +for just invoking the source:build task.

+

You can override the name of the built image and/or the destination registry +temporarily by providing a number of environment variables (see the +Taskfile). To permanently change these configurations, eg. in +a fork, change the defaults directly in the Taskfile.yml.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/translation-system/index.html b/DPL-CMS/translation-system/index.html new file mode 100644 index 0000000..ab13f06 --- /dev/null +++ b/DPL-CMS/translation-system/index.html @@ -0,0 +1,3081 @@ + + + + + + + + + + + + + + + + + + + + + + + Translation system - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Translation system

+ + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-CMS/translation/index.html b/DPL-CMS/translation/index.html new file mode 100644 index 0000000..569c3a4 --- /dev/null +++ b/DPL-CMS/translation/index.html @@ -0,0 +1,3336 @@ + + + + + + + + + + + + + + + + + + + + + + + Translation - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Translation

+

We manage translations as a part of the codebase using .po translation files. +Consequently translations must be part of either official or local translations +to take effect on the individual site.

+

DPL CMS is configured to use English as the master language but is configured +to use Danish for all users through language negotiation. This allows us to +follow a process where English is the default for the codebase but actual usage +of the system is in Danish.

+

Translation system

+

To make the "translation traffic" work following components are being used:

+
    +
  • GitHub
  • +
  • Stores .po files in git with + translatable strings and translations
  • +
  • GitHub Actions
  • +
  • Scans codebase for new translatable strings and commits them to GitHub
  • +
  • Exports translatable configuration strings into a separate *.config.po file
  • +
  • Merges the two files: *.po and *.config.po into a *.combined.po file
  • +
  • Notifies POEditor that new translatable strings are available
  • +
  • When a project is exported from POEditor:
      +
    • The *.combined.po is split into two files: *.po and *.config.po
    • +
    • The *.po files are published to GitHub Pages
    • +
    +
  • +
  • POEditor
  • +
  • Provides an interface for translators
  • +
  • Links translations with .po files on GitHub
  • +
  • Provides webhooks where external systems can notify of new translations
  • +
  • DPL CMS
  • +
  • Drupal installation which is configured to use GitHub Pages as an interface + translation server from which .po + files can be consumed.
  • +
  • In the development setup and in cronjobs defined in the environments there + are two jobs in charge of importing the regular translations + and the configuration translations.
  • +
+

The following diagram show how these systems interact to support the flow of +from introducing a new translateable string in the codebase to DPL CMS consuming +an updated translation with said string.

+

case

+
sequenceDiagram
+  Actor Translator
+  Actor Developer
+  Developer ->> Developer: Open pull request with new translatable string
+  Developer ->> GitHubActions: Merge pull request into develop
+  GitHubActions ->> GitHubActions: Scan codebase and write strings to .po file
+  GitHubActions ->> GitHubActions: Fill .po file with existing translations
+%% <!-- markdownlint-disable-next-line MD013 -->
+  Note over GitHubActions,GitHubActions: If config translations<br/>are available<br/>they are used<br/>otherwise empty strings
+%% <!-- markdownlint-disable-next-line MD013 -->
+  GitHubActions ->> GitHubActions: Exports configuration translations into a .config.po file
+%% <!-- markdownlint-disable-next-line MD013 -->
+  GitHubActions ->> GitHubActions: The two .po files are merged together into a .combined.po file
+  GitHubActions ->> GitHub: Commit combined.po file with updated strings
+  GitHubActions ->> POEditor: Call webhook
+  POEditor ->> GitHub: Fetch updated combined.po file
+  POEditor ->> POEditor: Synchronize translations with latest strings and translations
+  Translator ->> POEditor: Translate strings
+  Translator ->> POEditor: Export strings to GitHub
+  POEditor ->> GitHub: Commit combined.po file with updated translations to develop
+  GitHub ->> GitHub: .combined.po is split into two files: .po and .config.po
+  GitHub ->> GitHub: All the po files are published to Github Pages
+  DplCms ->> GitHub: Fetch .po file with latest translations
+  DplCms ->> DplCms: Import updated translations
+  DplCms ->> GitHub: Import config.po file with latest configuration translations
+

Howtos

+

Add new or update existing translation

+
    +
  1. Log into POEditor.com and go to the dpl-cms project
  2. +
  3. Go to the relevant language
  4. +
  5. Locate the string (term) to be translated
  6. +
  7. Translate the string
  8. +
+

Publish updated translations

+
    +
  1. Log into POEditor.com
  2. +
  3. Select the "Settings" tab
  4. +
  5. Click the GitHub code hosting service
  6. +
  7. Check the relevant language(s)
  8. +
  9. Select "Export to GitHub" and click "Go"
  10. +
+

Import updated translations

+
    +
  1. Run drush locale-check
  2. +
  3. Run drush locale-update
  4. +
+

Import updated config translations

+

Run drush dpl_po:import-remote-config-po [LANGUAGE_CODE] [CONFIGURATION_PO_FIL_EXTERNAL_URL]

+

Example:

+
drush dpl_po:import-remote-config-po da https://danskernesdigitalebibliotek.github.io/dpl-cms/translations/da.config.po
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Design-System/Icon-guidelines/index.html b/DPL-Design-System/Icon-guidelines/index.html new file mode 100644 index 0000000..f273c0d --- /dev/null +++ b/DPL-Design-System/Icon-guidelines/index.html @@ -0,0 +1,3105 @@ + + + + + + + + + + + + + + + + + + + + + + + Icon Usage Guidelines - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Icon Usage Guidelines

+

This folder contains SVG icons that are used in the design system. When using +icons from this folder, please follow the guidelines below:

+
    +
  • +

    Icons located in public/ folder is the current source of truth. All icons +will be placed in this folder. Ensure that icons placed gere are using the +class attribute, and not className for SVG element.

    +
  • +
  • +

    For icons used in React components that require class modifications or +similar, first create the icon in public/icons, and then manually copy the SVG +from the public/icons folder and make any necessary changes, i.e replacing +the class with className or other modifications.

    +
  • +
+

Currently, there is no automated solution for this process that ensures +consistency for this. This may be changed in the future.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Design-System/architecture/adr-001-skeleton-screens/index.html b/DPL-Design-System/architecture/adr-001-skeleton-screens/index.html new file mode 100644 index 0000000..664f868 --- /dev/null +++ b/DPL-Design-System/architecture/adr-001-skeleton-screens/index.html @@ -0,0 +1,3261 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Skeleton Screens - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Skeleton Screens

+

Context

+

In the work of trying to improve the performance of the search results +we needed a way to fill the viewport with a simulated interface in order to:

+
    +
  • Show some content immediately to the user
  • +
  • Prevent layout shifting between loading state and ready state
  • +
+

Decision

+

We decided to implement skeleton screens when loading data. The skeleton screens +are rendered in pure css. +The css classes are coming from the library: skeleton-screen-css

+

Alternatives considered

+

The library is very small and based on simple css rules, so we could have +considered replicating it in our own design system or make something similar. +But by using the open source library we are ensured, to a certain extent, +that the code is being maintained, corrected and evolves as time goes by.

+

We could also have chosen to use images or GIF's to render the screens. +But by using the simple toolbox of skeleton-screen-css we should be able +to make screens for all the different use cases in the different apps.

+

Consequences

+

It is now possible, with a limited amount of work, to construct skeleton screens +in the loading state of the various user interfaces.

+

Because we use library where skeletons are implemented purely in CSS +we also provide a solution which can be consumed in any technology +already using the design system without any additional dependencies, +client side or server side.

+

BEM rules when using Skeleton Screen Classes in dpl-design-system

+

Because we want to use existing styling setup in conjunction +with the Skeleton Screen Classes we sometimes need to ignore the existing +BEM rules that we normally comply to. +See eg. the search result styling.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Design-System/architecture/adr-002-form-styling/index.html b/DPL-Design-System/architecture/adr-002-form-styling/index.html new file mode 100644 index 0000000..ec2bd65 --- /dev/null +++ b/DPL-Design-System/architecture/adr-002-form-styling/index.html @@ -0,0 +1,3256 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Form Styling - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Form Styling

+

Context

+

There are various types of forms within the project, and it is always a dilemma +as to whether to write specific styling per form, or to create a common set of +base classes.

+

Decision

+

We have decided to create a set of default classes to be used when building +different kinds of forms, as to not create a large amount of location that +contain form styling. Considering the forms within the project all look very +similar/consist of elements that look the same, it will be an advantage to have +a centralized place to expand/apply future changes to.

+

As we follow the BEM class structure, the block is called dpl-form, which can +be expanded with elements, and modifiers.

+

Alternatives considered

+

We considered writing new classes every time we introduced a new form, however, +this seemed like the inferior option. If a specific form element was to change +styling in the future, we would have to adjust all of the specific instances, +instead of having a singular definition. And in case a specific instance needs +to adopt a different styling, it can be achieved by creating a specific class +fot that very purpose.

+

Consequences

+

As per this decision, we expect introduction of new form elements to be styled +expanding the current dpl-form class.

+

This currently has an exception in form of form inputs - these have been styled +a long time ago and use the class dpl-input.

+

Implementation in the dpl-design-system

+

Here is the link to our form css file.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Design-System/index.html b/DPL-Design-System/index.html new file mode 100644 index 0000000..269a725 --- /dev/null +++ b/DPL-Design-System/index.html @@ -0,0 +1,3567 @@ + + + + + + + + + + + + + + + + + + + + + + + DPL Design System - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL Design System

+

DPL Design System is a library of UI components that should be used as a common +base system for "Danmarks Biblioteker" / "Det Digitale Folkebibliotek". The +design is implemented +with Storybook +/ React and is output with HTML markup and css-classes +through an addon in Storybook.

+

The codebase follows the naming that designers have used in Figma closely +to ensure consistency.

+

Requirements

+

This project comes with go-task and docker +compose, hence the requirements are limited to having docker install and tasks.

+

Manual requirements

+

This project can be used outside docker with the following requirements:

+
    +
  • node 16
  • +
  • yarn
  • +
+

Check in the terminal which versions you have installed with node -v.

+

Installation

+

Use the tasks defined in Taskfile to run the project:

+
task dev:install
+
+

Installation outside docker

+

Use the node package manager to install project dependencies:

+
yarn install
+
+

Development

+

To start the docker compose setup in development simple use the start task:

+
task dev:start
+
+

To see the output from the compile process and start of storybook:

+
task dev:logs
+
+

Use task and tabulator key in the terminal to see the other predefined tasks:

+
task dev:[TAB]
+
+

Development without docker

+

To start developing run:

+
yarn dev
+
+

Components and CSS will be automatically recompiled when making changes in the +source code.

+

Usage

+

The project is available in two ways and should be consumed accordingly:

+
    +
  1. As package in the local npm registry for this repository
  2. +
  3. As a dist.zip file attached to a release for this repository
  4. +
+

Both releases contain the built assets of the project: JavaScript files, CSS +styles and icons.

+

You can find the HTML output for a given story under the HTML tab inside +storybook.

+

NPM package

+

The GitHub NPM package registry requires authentication if you are to access +packages there.

+

Consequently, if you want to use the design system as an NPM package or if you +use a project that depends on the design system as an NPM package you must +authenticate:

+
    +
  1. Create a GitHub token with the required scopes: repo and read:packages
  2. +
  3. Run npm login --registry=https://npm.pkg.github.com
  4. +
  5. Enter the following information:
  6. +
+
> Username: [Your GitHub username]
+> Password: [Your GitHub token]
+> Email: [An email address used with your GitHub account]
+
+

Note that you will need to reauthenticate when your personal access token +expires.

+

Deployment and releases

+

The project is automatically built and deployed +on pushes to every branch and every tag and the result is available as releases +which support both types of usage. This applies for the original +repository on GitHub and all GitHub forks.

+

You can follow the status of deployments in the Actions list for the repository +on GitHub. +The action logs also contain additional details regarding the contents and +publication of each release. If using a fork then deployment actions can be +seen on the corresponding list.

+

In general consuming projects should prefer tagged releases as they are stable +proper releases.

+

During development where the design system is being updated in parallel with +the implementation of a consuming project it may be advantageous to use a +release tagging a branch.

+

Tagged releases

+

Run the following to publish a tag and create a release:

+
git tag -a v*.*.* && git push origin v*.*.*
+
+

Usage: npm package

+

In the consuming project update usage to the new release:

+
npm install @danskernesdigitalebibliotek/dpl-design-system@*.*.*
+
+

Usage: Release file

+

Find the release for the tag on the releases page on GitHub +and download the dist.zip file from there and use it as needed in the +consuming project.

+

Branch releases

+

The project automatically creates a release for each branch.

+

Example: Pushing a commit to a new branch feature/reservation-modal will +create the following parts:

+
    +
  1. A git tag for the commit release-feature/reservation-modal. A tag is needed + to create a GitHub release.
  2. +
  3. A GitHub release for the called feature/reservation-modal. The build is + attached here.
  4. +
  5. A package in the local npm repository tagged feature-reservation-modal. + Special characters like / are not supported by npm tags and are converted + to -.
  6. +
+

Updating the branch will update all parts accordingly.

+

Usage: npm package

+

In the consuming project update usage to the new release:

+
npm install @danskernesdigitalebibliotek/dpl-design-system@feature-reservation-modal
+
+

If your release belongs to a fork you can use aliasing +to point to the release of the package in the npm repository for the fork:

+
npm config set @my-fork:registry=https://npm.pkg.github.com
+npm install @danskernesdigitalebibliotek/dpl-design-system@npm:@my-fork/dpl-design-system@feature-reservation-modal
+
+

This will update your package.json and lock files accordingly. Note that +branch releases use temporary versions in the format 0.0.0-[GIT-SHA] and you +may in practice see these referenced in both files.

+

If you push new code to the branch you have to update the version used in the +consuming project:

+
npm update @danskernesdigitalebibliotek/dpl-design-system
+
+

Aliasing, +repository configuration and +updating installed packages +are also supported by Yarn.

+

Usage: Release file

+

Find the release for the branch on the releases page on GitHub +and download the dist.zip file from there and use it as needed in the +consuming project.

+

If your branch belongs to a fork then you can find the release on the releases +page for the fork.

+

Repeat the process if you push new code to the branch.

+

Storybook

+

Spin up storybook by running this command in the terminal:

+
yarn storybook
+
+

When storybook is ready it automatically opens up in a browser with the +interface ready to use.

+

Chromatic

+

We are using Chromatic for visual test. You can access the dashboard +under the danskernesdigitalebibliotek (organisation) dpl-design-system +(project).

+

https://www.chromatic.com/builds?appId=616ffdab9acbf5003ad5fd2b

+

You can deploy a version locally to Chromatic by running:

+
yarn chromatic
+
+

Make sure to set the CHROMATIC_PROJECT_TOKEN environment variable is available +in your shell context. You can access the token from:

+

https://www.chromatic.com/manage?appId=616ffdab9acbf5003ad5fd2b&view=configure

+

What is Storybook

+

Storybook is an +open source tool for building UI components and pages in isolation from your +app's business logic, data, and context. Storybook helps you document components +for reuse and automatically visually test your components to prevent bugs. It +promotes the component-driven process and agile +development.

+

It is possible to extend Storybook with an ecosystem of addons that help you do +things like fine-tune responsive layouts or verify accessibility.

+

How to use

+

The Storybook interface is simple and intuitive to use. Browse the project's +stories now by navigating to them in the sidebar.

+

The stories are placed in a flat structure, where developers should not spend +time thinking of structure, since we want to keep all parts of the system under +a heading called Library. This Library is then dividid in folders where common +parts are kept together.

+

To expose to the user how we think these parts stitch together for example +for the new website, we have a heading called Blocks, to resemble what cms +blocks a user can expect to find when building pages in the choosen CMS.

+

This could replicate in to mobile applications, newsletters etc. all pulling +parts from the Library.

+

Each story has a corresponding .stories file. View their code in the +src/stories directory to learn how they work. +The stories file is used to add the component to the Storybook interface via +the title. Start the title with "Library" or "Blocks" and use / to +divide into folders fx. Library / Buttons / Button

+

Addons

+

Storybook ships with some essential +pre-installed addons +to power the core Storybook experience.

+ +

There are many other helpful addons to customise the usage and experience. +Additional addons used for this project:

+
    +
  • +

    HTML / storybook-addon-html: + This addon is used to display compiled HTML markup for each story and make it + easier for developers to grab the code. Because we are developing with React, + it is necessary to be able to show the HTML markup with the css-classes to + make it easier for other developers that will implement it in the future. + If a story has controls the HTML markup changes according to the controls that + are set.

    +
  • +
  • +

    Designs / storybook-addon-designs: + This addon is used to embed Figma in the addon panel for a better + design-development workflow.

    +
  • +
  • +

    A11Y: + This addon is used to check the accessibility of the components.

    +
  • +
+

All the addons can be found in storybook/main.js directory.

+

Important to notice

+
Internal classes
+

To display some components (fx Colors, Spacing) in a more presentable way, we +are using some "internal" css-classes which can be found in the +styles/internal.scss file. All css-classes with "internal" in the front should +therefore be ignored in the HTML markup.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Design-System/layout-documentation/index.html b/DPL-Design-System/layout-documentation/index.html new file mode 100644 index 0000000..503c80c --- /dev/null +++ b/DPL-Design-System/layout-documentation/index.html @@ -0,0 +1,3374 @@ + + + + + + + + + + + + + + + + + + + + + + + Project Layout documentation & Examples - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Project Layout documentation & Examples

+

Table of Contents

+ +

Introduction

+

This documentation provides detailed information on the CSS/SCSS standards for +the 'formidling' project. It covers the usage of mixins for layout and spacing, +as well as guidelines for responsive design.

+

Note on Applicability

+

This document and its standards apply specifically to elements of the +'formidling' project starting from December 1, 2023, and onwards. It has been +created to aligns with the recent developments in the project and is not +retroactively applied to earlier components or structures.

+

Layout related SCSS is defined in the variables.layout.scss file.

+

If anything in variables.layout.scss is modified or extended, make sure you +include your changes in this documentation.

+

Variables

+

The file defines several variables for managing layout and spacing:

+
    +
  • $layout__max-width--*: These variables store the maximum width values +aligned with breakpoints.
  • +
  • block__max-width--*: These variables store the maximum width values +between block (paragraph) elements.
  • +
  • $layout__edge-spacing: This variable stores the edge spacing (padding) +value for containers.
  • +
+

All components that require a max-width should use the block__max-width--* +variables along with layout-container.

+

Mixins

+

The file defines two mixins:

+
    +
  • +

    layout-container($max-width, $padding): This mixin sets the maximum width, + and padding of an element, as well as centering them.

    +
  • +
  • +

    block-spacing($modifier): This manages the vertical margins of +elements, allowing for consistent spacing throughout the project. Use $modifer +to create negative/sibling styles.

    +
  • +
+

Vertical padding is not a part of layout.scss. Use regular $spacing +variables for that.

+

block-spacing($modifier) is an approach use for the CMS, where all +paragraphs are rendered with a wrapper that will automatically add necessary +spacing between the components. Any component that is not a paragraph, should +therefore follow this approach by including the mixin @mixin block-spacing

+

The $modfier is currently a either

+
    +
  • sibling used for add a -1px to remove borders between sibling elements.
  • +
  • negative used for elements that require -margin instead.
  • +
+

Example Usage

+

Here are some examples of how to use these mixins, and utility classes.

+

Including mixins in components using BEM

+
// Using mixins in your BEM-named parent container.
+.your-BEM-component-name {
+  @include layout-container;
+
+  @include media-query__small() {
+    // Applying new edge spacing (Padding) using $spacings / other.
+    @include layout-container($padding: $s-xl);
+  }
+
+ @include media-query__large() {
+    // Removing max-width & applying specific padding from $spacings / other
+     @include layout-container($max-width: 0, $padding: $s-md);
+  }
+}
+
+

Adding spacing to non-paragraph elements in the cms

+
// Using block-spacing for non-paragraph component or container.
+.your-BEM-component-name {
+  @include block-spacing;
+}
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Design-System/scss/index.html b/DPL-Design-System/scss/index.html new file mode 100644 index 0000000..addf5da --- /dev/null +++ b/DPL-Design-System/scss/index.html @@ -0,0 +1,3305 @@ + + + + + + + + + + + + + + + + + + + + + + + SCSS strategy - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

SCSS strategy

+

In December 2023, we have aimed to streamline the way we write SCSS. +Some of these rules have not been applied on previous code, but moving forward, +this is what we aim to do.

+

BEM naming convention

+

Examples of do's and dont's

+

Assuming we have a Counter block:

+
    +
  • Styling must be placed in a correspondingly named file counter.scss
  • +
  • .counter__title
  • +
  • &__title ❌ (&__ should be avoided, to avoid massive indention.)
  • +
  • .counter-title ❌ (Must start with .FILE-NAME__)
  • +
  • .counter__title__text ❌ (Only one level)
  • +
+

Variants and modifiers

+

Sometimes you'll want to add variants to CSS-only classes. This can be done +using modifier classes - e.g. .counter--large, .counter__title--large. +These classes must not be set alone. E.g. .counter__title--large must not +exist on an element without also having .counter__title.

+

Mixins, placeholder and variables

+

Shared tooling is saved in src/styles/scss/tools, +NOT in individual stories.

+

Typography

+

Typography is defined in +src/styles/scss/tools/variables.typography.scss. +These variables, all starting with $typo__, can be used, using a mixin, +@include typography($typo__h2); in stories. +Generally speaking, font styling should be avoided directly in stories, rather +adding new variants in the variables.typography.scss file. +This way, we can better keep track of what is available, and avoid duplicate +styling in the future.

+

Legacy classes

+

In the future, we want to apply these rules to old code too. Until then, +the old classes are supported using the files +in src/styles/scss/legacy.

+

These classes should not be used in new components

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/Taskfile.yml b/DPL-Platform/Taskfile.yml new file mode 100644 index 0000000..0f26dee --- /dev/null +++ b/DPL-Platform/Taskfile.yml @@ -0,0 +1,63 @@ +version: "3" + +vars: + PLANTUML_RENDERER_VERSION: 1.2021.5 + DRAWIO_EXPORT_VERSION: 4.1.0 + +tasks: + _mkdir: + cmds: + - mkdir -p diagrams/render-png + - mkdir -p diagrams/render-svg + + clean: + desc: Delete all rendered diagrams + cmds: + - rm -fr diagrams/render-png + - rm -fr diagrams/render-svg + + render: + desc: Render all diagrams + cmds: + - task: render:plantuml + - task: render:drawio + + build:plantuml: + desc: Build the container image we use for rendering plantuml + dir: ../tools/plantuml + cmds: + # We do not publish the image as it is very then wrapper around a download + # of platuml an as such having a published image would just be an extra + # thing to keep track of. + - IMAGE_URL=plantuml TAG=0.0.0 PLANTUML_VERSION={{.PLANTUML_RENDERER_VERSION}} task build + + render:plantuml: + desc: Render svg and png versions plantuml diagrams + deps: [_mkdir, build:plantuml] + cmds: + # PDF is currently not supported: https://plantuml.com/pdf + - | + docker run \ + -v "${PWD}/diagrams/:/checkout" \ + -w "/checkout" \ + plantuml:0.0.0 \ + -verbose -tpng -o render-png *.puml + + - | + docker run \ + -v "${PWD}/diagrams/:/checkout" \ + -w "/checkout" \ + plantuml:0.0.0 \ + -verbose -tsvg -o render-svg *.puml + + render:drawio: + desc: Render svg and png versions drawio diagrams + deps: [_mkdir] + cmds: + - | + docker run \ + -v "${PWD}/diagrams:/data" rlespinasse/drawio-export:{{.DRAWIO_EXPORT_VERSION}} --remove-page-suffix --format png --output render-png --scale 2 + + - | + docker run \ + -v "${PWD}/diagrams:/data" rlespinasse/drawio-export:{{.DRAWIO_EXPORT_VERSION}} --remove-page-suffix --format svg --output render-svg --scale 2 \ No newline at end of file diff --git a/DPL-Platform/architecture/adr/adr-001-lagoon/index.html b/DPL-Platform/architecture/adr/adr-001-lagoon/index.html new file mode 100644 index 0000000..2ba4179 --- /dev/null +++ b/DPL-Platform/architecture/adr/adr-001-lagoon/index.html @@ -0,0 +1,3224 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Lagoon - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Lagoon

+

Context

+

The Danish Libraries needed a platform for hosting a large number of Drupal +installations. As it was unclear exactly how to build such a platform and how +best to fulfill a number of requirements, a Proof Of Concept project was +initiated to determine whether to use an existing solution or build a platform +from scratch.

+

After an evaluation, Lagoon was chosen.

+

Decision

+

The main factors behind the decision to use Lagoon where:

+
    +
  • Much lower cost of maintenance than a self-built platform.
  • +
  • The platform is continually updated, and the updates are available for free.
  • +
  • A well-established platform with a lot of proven functionality right out of + the box.
  • +
  • The option of professional support by Amazee
  • +
+

When using and integrating with Lagoon we should strive to

+
    +
  • Make as little modifications to Lagoon as possible
  • +
  • Whenever possible, use the defaults, recommendations and best practices + documented on eg. docs.lagoon.sh
  • +
+

We do this to keep true to the initial thought behind choosing Lagoon as a +platform that gives us a lot of functionality for a (comparatively) small +investment.

+

Alternatives considered

+

The main alternative that was evaluated was to build a platform from scratch. +While this may have lead to a more customized solution that more closely matched +any requirements the libraries may have, it also required a very large +investment would require a large ongoing investment to keep the platform +maintained and updated.

+

We could also choose to fork Lagoon, and start making heavy modifications to the +platform to end up with a solution customized for our needs. The downsides of +this approach has already been outlined.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/architecture/adr/adr-002-rightsizing/index.html b/DPL-Platform/architecture/adr/adr-002-rightsizing/index.html new file mode 100644 index 0000000..b67e8b1 --- /dev/null +++ b/DPL-Platform/architecture/adr/adr-002-rightsizing/index.html @@ -0,0 +1,3429 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Rightsizing - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Architecture Decision Record: Rightsizing

+

Context

+

Expected traffic

+

The request path and expected traffic +The platform is required to be able to handle an estimated 275.000 page-views +per day spread out over 100 websites. A visit to a website that causes the +browser to request a single html-document followed by a number of assets is only +counted as a single page-view.

+

On a given day about half of the page-views to be made by an authenticated +user. We further more expect the busiest site receive about 12% of the traffic.

+

Given these numbers, we can make some estimates of the expected average load. +To stay fairly conservative we will still assume that about 50% of the traffic +is anonymous and can thus be cached by Varnish, but we will assume that all +all sites gets traffic as if they where the most busy site on the platform (12%).

+

12% of 275.000 requests gives us a an average of 33.000 requests. To keep to the +conservative side, we concentrate the load to a period of 8 hours. We then end +up with roughly 1 page-view pr. second.

+

Expected workload characteristics

+

The platform is hosted on a Kubernetes cluster on which Lagoon is installed. +As of late 2021, Lagoons approach to handling rightsizing of PHP- and in +particular Drupal-applications is based on a number factors:

+
    +
  1. Web workloads are extremely spiky. While a site looks to have to have a + sustained load of 5 rps when looking from afar, it will in fact have anything + from (eg) 0 to 20 simultaneous users on a given second.
  2. +
  3. Resource-requirements are every ephemeral. Even though a request as a peak + memory-usage of 128MB, it only requires that amount of memory for a very + short period of time.
  4. +
  5. Kubernetes nodes has a limit of how many pods will fit on a given node. + This will constraint the scheduler from scheduling too many pods to a node + even if the workloads has declared a very low resource request.
  6. +
  7. With metrics-server enabled, Kubernetes will keep track of the actual + available resources on a given node. So, a node with eg. 4GB ram, hosting + workloads with a requested resource allocation of 1GB, but actually taking + up 3.8GB of ram, will not get scheduled another pod as long as there are + other nodes in the cluster that has more resources available.
  8. +
+

The consequence of the above is that we can pack a lot more workload onto a single +node than what would be expected if you only look at the theoretical maximum +resource requirements.

+

Lagoon resource request defaults

+

Lagoon sets its resource-requests based on a helm values default in the +kubectl-build-deploy-dind image. The default is typically 10Mi pr. container +which can be seen in the nginx-php chart which runs a php and +nginx container. Lagoon configures php-fpm to allow up to 50 children +and allows php to use up to 400Mi memory.

+

Combining these numbers we can see that a site that is scheduled as if it only +uses 20 Megabytes of memory, can in fact take up to 20 Gigabytes. The main thing +that keeps this from happening in practice is a a combination of the above +assumptions. No node will have more than a limited number of pods, and on a +given second, no site will have nearly as many inbound requests as it could have.

+

Decision

+

Lagoon is a very large and complex solution. Any modification to Lagoon will +need to be well tested, and maintained going forward. With this in mind, we +should always strive to use Lagoon as it is, unless the alternative is too +costly or problematic.

+

Based on real-live operations feedback from Amazee (creators of Lagoon) and the +context outline above we will

+
    +
  • Leave the Lagoon defaults as they are, meaning most pods will request 10Mi of + memory.
  • +
  • Let the scheduler be informed by runtime metrics instead of up front pod + resource requests.
  • +
  • Rely on the node maximum pods to provide some horizontal spread of pods.
  • +
+

Alternatives considered

+

As Lagoon does not give us any manual control over rightsizing out of the box, +all alternatives involves modifying Lagoon.

+

Altering Lagoon Defaults

+

We've inspected the process Lagoon uses to deploy workloads, and determined that +it would be possible to alter the defaults used without too many modifications.

+

The build-deploy-docker-compose.sh script that renders the manifests that +describes a sites workloads via Helm includes a +service-specific values-file. This file can be used to +modify the defaults for the Helm chart. By creating custom container-image for +the build-process based on the upstream Lagoon build image, we can deliver our +own version of this image.

+

As an example, the following Dockerfile will add a custom values file for the +redis service.

+
FROM docker.io/uselagoon/kubectl-build-deploy-dind:latest
+COPY redis-values.yaml /kubectl-build-deploy/
+
+

Given the following redis-values.yaml

+
resources:
+  requests:
+    cpu: 10m
+    memory: 100Mi
+
+

The Redis deployment would request 100Mi instead of the previous default of 10Mi.

+

Introduce "t-shirt" sizes

+

Building upon the modification described in the previous chapter, we could go +even further and modify the build-script itself. By inspecting project variables +we could have the build-script pass in eg. a configurable value for +replicaCount for a pod. This would allow us to introduce a +small/medium/large concept for sites. This could be taken even further to eg. +introduce whole new services into Lagoon.

+

Consequences

+

This could lead to problems for sites that requires a lot of resources, but +given the expected average load, we do not expect this to be a problem even if +a site receives an order of magnitude more traffic than the average.

+

The approach to rightsizing may also be a bad fit if we see a high concentration +of "non-spiky" workloads. We know for instance that Redis and in particular +Varnish is likely to use a close to constant amount of memory. Should a lot of +Redis and Varnish pods end up on the same node, evictions are very likely to +occur.

+

The best way to handle these potential situations is to be knowledgeable about +how to operate Kubernetes and Lagoon, and to monitor the workloads as they are +in use.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/architecture/adr/adr-003-system-alerts/index.html b/DPL-Platform/architecture/adr/adr-003-system-alerts/index.html new file mode 100644 index 0000000..c0f7a86 --- /dev/null +++ b/DPL-Platform/architecture/adr/adr-003-system-alerts/index.html @@ -0,0 +1,3209 @@ + + + + + + + + + + + + + + + + + + + + + + + ADR-003 System alerts - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

ADR-003 System alerts

+

Context

+

There has been a wish for a functionality that alerts administrators if certain +system values have gone beyond defined thresholds rules.

+

Decision

+

We have decided to use alertmanager +that is a part of the Prometheus package that is +already used for monitoring the cluster.

+

Consequences

+
    +
  • We have tried to install alertmanager and testing it. + It works and given the various possibilities of defining + alert rules we consider + the demands to be fulfilled.
  • +
  • We will be able to get alerts regarding thresholds on both container and + cluster level which is what we need.
  • +
  • Alertmanager fits in the general focus of being cloud agnostic. It is + CNCF approved + and does not have any external infrastructure dependencies.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/architecture/adr/adr-004-declarative-site-management/index.html b/DPL-Platform/architecture/adr/adr-004-declarative-site-management/index.html new file mode 100644 index 0000000..677c92a --- /dev/null +++ b/DPL-Platform/architecture/adr/adr-004-declarative-site-management/index.html @@ -0,0 +1,3262 @@ + + + + + + + + + + + + + + + + + + + + + + + ADR 004: Declarative Site management - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

ADR 004: Declarative Site management

+

Context

+

Lagoon requires a site to be deployed from at Git repository containing a +.lagoon.yml and docker-compose.yml +A potential logical consequence of this is that we require a Git repository pr +site we want to deploy, and that we require that repository to maintain those +two files.

+

Administering the creation and maintenance of 100+ Git repositories can not be +done manually with risk of inconsistency and errors. The industry best practice +for administering large-scale infrastructure is to follow a declarative +Infrastructure As Code(IoC) +pattern. By keeping the approach declarative it is much easier for automation +to reason about the intended state of the system.

+

Further more, in a standard Lagoon setup, Lagoon is connected to the "live" +application repository that contains the source-code you wish to deploy. In this +approach Lagoon will just deploy whatever the HEAD of a given branch points. In +our case, we perform the build of a sites source release separate from deploying +which means the sites repository needs to be updated with a release-version +whenever we wish it to be updated. This is not a problem for a small number of +sites - you can just update the repository directly - but for a large set of +sites that you may wish to administer in bulk - keeping track of which version +is used where becomes a challenge. This is yet another good case for declarative +configuration: Instead of modifying individual repositories by hand to deploy, +we would much rather just declare that a set of sites should be on a specific +release and then let automation take over.

+

While there are no authoritative discussion of imperative vs declarative IoC, +the following quote from an OVH Tech Blog +summarizes the current consensus in the industry pretty well:

+
+

In summary declarative infrastructure tools like Terraform and CloudFormation +offer a much lower overhead to create powerful infrastructure definitions that +can grow to a massive scale with minimal overheads. The complexities of hierarchy, +timing, and resource updates are handled by the underlying implementation so +you can focus on defining what you want rather than how to do it.

+

The additional power and control offered by imperative style languages can be +a big draw but they also move a lot of the responsibility and effort onto the +developer, be careful when choosing to take this approach.

+
+

Decision

+

We administer the deployment to and the Lagoon configuration of a library site +in a repository pr. library. The repositories are provisioned via Terraform that +reads in a central sites.yaml file. The same file is used as input for the +automated deployment process which renderers the various files contained in the +repository including a reference to which release of DPL-CMS Lagoon should use.

+

It is still possible to create and maintain sites on Lagoon independent of this +approach. We can for instance create a separate project for the dpl-cms +repository to support core development.

+

Status

+

Accepted

+

Alternatives considered

+

We could have run each site as a branch of off a single large repository. +This was rejected as a possibility as it would have made the administration of +access to a given libraries deployed revision hard to control. By using individual +repositories we have the option of grating an outside developer access to a +full repository without affecting any other.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/architecture/adr/adr-005-declarative-site-management-2/index.html b/DPL-Platform/architecture/adr/adr-005-declarative-site-management-2/index.html new file mode 100644 index 0000000..dc6ca06 --- /dev/null +++ b/DPL-Platform/architecture/adr/adr-005-declarative-site-management-2/index.html @@ -0,0 +1,3229 @@ + + + + + + + + + + + + + + + + + + + + + + + ADR 005: Declarative Site management (2) - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

ADR 005: Declarative Site management (2)

+

Context

+

We have previously established (in ADR 004) +that we want to take a declarative approach to site management. The previous +ADR focuses on using a single declarative file, sites.yaml, for driving +Github repositories that can be used by Lagoon to run sites.

+

The considerations from that ADR apply equally here. We have identified more +opportunities to use a declarative approach, which will simultaneously +significantly simplify the work required for platform maintainers when managing +sites.

+

Specifically, runbooks with several steps to effectuate a new deployment are a +likely source of errors. The same can be said of having to manually run +commands to make changes in the platform.

+

Every time we run a command that is not documented in the source code in the +platform main branch, it becomes less clear what the state of the platform is.

+

Conversely, every time a change is made in the main branch that has not yet +been executed, it becomes less clear what the state of the platform is.

+

Decision

+

We continuously strive towards making the main branch in the dpl-platform repo +the single source of truth for what the state of the platform should be. The +repository becomes the declaration of the entire state.

+

This leads to at least two concrete steps:

+
    +
  • +

    We will automate synchronizing state in the platform-repo with the actual + platform state. This means using sites.yaml to declare the expected state + in Lagoon (e.g. which projects and environments are created), not just the + Github repos. This leaves the deployment process less error prone.

    +
  • +
  • +

    We will automate running the synchronization step every time code is checked + into the main branch. This means state divergence between the platform repo + declaration and reality is minimized.

    +
  • +
+

It will still be possible for dpl-cms to maintain its own area of state +in the platform, but anything declared in dpl-platform will be much more +likely to be the actual state in the platform.

+

Status

+

Proposed

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/architecture/adr/index.html b/DPL-Platform/architecture/adr/index.html new file mode 100644 index 0000000..83396e1 --- /dev/null +++ b/DPL-Platform/architecture/adr/index.html @@ -0,0 +1,3136 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Records - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Records

+

We loosely follow the guidelines for ADRs described by Michael Nygard.

+

A record should attempt to capture the situation that led to the need for a +discrete choice to be made, and then proceed to describe the core of the +decision, its status and the consequences of the decision.

+

To summaries a ADR could contain the following sections (quoted from the above +article):

+
    +
  • +

    Title: These documents have names that are short noun phrases. For example, + "ADR 1: Deployment on Ruby on Rails 3.0.10" or "ADR 9: LDAP for Multitenant Integration"

    +
  • +
  • +

    Context: This section describes the forces at play, including technological + , political, social, and project local. These forces are probably in tension, + and should be called out as such. The language in this section is value-neutral. + It is simply describing facts.

    +
  • +
  • +

    Decision: This section describes our response to these forces. It is stated + in full sentences, with active voice. "We will …"

    +
  • +
  • +

    Status: A decision may be "proposed" if the project stakeholders haven't + agreed with it yet, or "accepted" once it is agreed. If a later ADR changes + or reverses a decision, it may be marked as "deprecated" or "superseded" with + a reference to its replacement.

    +
  • +
  • +

    Consequences: This section describes the resulting context, after applying + the decision. All consequences should be listed here, not just the "positive" + ones. A particular decision may have positive, negative, and neutral consequences, + but all of them affect the team and project in the future.

    +
  • +
+ + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/architecture/alertmanager-setup/index.html b/DPL-Platform/architecture/alertmanager-setup/index.html new file mode 100644 index 0000000..40b34c6 --- /dev/null +++ b/DPL-Platform/architecture/alertmanager-setup/index.html @@ -0,0 +1,3253 @@ + + + + + + + + + + + + + + + + + + + + + + + Alertmanager Setup - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Alertmanager Setup

+

We use the alertmanager which automatically +ties to the metrics of Prometheus but in order to make it work the configuration +and rules need to be setup.

+

Configuration

+

The configuration is stored in a secret:

+
kubectl get secret \
+  -n prometheus alertmanager-promstack-kube-prometheus-alertmanager -o yaml
+
+

In order to update the configuration you need to get the secret resource definition +yaml output and retrieve the data.alertmanager.yaml property.

+

You need to base64 decode the value, update configuration with SMTP settings, +receivers and so forth.

+

Rules

+

It is possible to set up various rules(thresholds), both on cluster level and for +separate containers and namespaces.

+

Here is a site with examples of rules to get an idea of the +possibilities.

+

Test

+

We have tested the setup by making a configuration looking like this:

+

Get the configuration form the secret as described above.

+

Change it with smtp settings in order to be able to debug the alerts:

+
global:
+  resolve_timeout: 5m
+  smtp_smarthost: smtp.gmail.com:587
+  smtp_from: xxx@xxx.xx
+  smtp_auth_username: xxx@xxx.xx
+  smtp_auth_password: xxxx
+receivers:
+- name: default
+- name: email-notification
+  email_configs:
+    - to: xxx@xxx.xx
+route:
+  group_by:
+  - namespace
+  group_interval: 5m
+  group_wait: 30s
+  receiver: default
+  repeat_interval: 12h
+  routes:
+  - match:
+      alertname: testing
+    receiver: email-notification
+  - match:
+      severity: critical
+    receiver: email-notification
+
+

Base64 encode the configuration and update the secret with the new configuration +hash.

+

Find the cluster ip of the alertmanager service running (the service name can +possibly vary):

+
kubectl get svc -n prometheus promstack-kube-prometheus-alertmanager
+
+

And then run a curl command in the cluster (you need to find the IP o):

+
# 1.
+kubectl run -i --rm --tty debug --image=curlimages/curl --restart=Never -- sh
+
+# 2
+curl -XPOST http://[ALERTMANAGER_SERVICE_CLUSTER_IP]:9093/api/v1/alerts \
+ -d '[{"status": "firing","labels": {"alertname": "testing","service": "curl",\
+ "severity": "critical","instance": "0"},"annotations": {"summary": \
+ "This is a summary","description": "This is a description."},"generatorURL": \
+ "http://prometheus.int.example.net/<generating_expression>",\
+ "startsAt": "2020-07-22T01:05:38+00:00"}]'
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/architecture/index.html b/DPL-Platform/architecture/index.html new file mode 100644 index 0000000..16cc495 --- /dev/null +++ b/DPL-Platform/architecture/index.html @@ -0,0 +1,3097 @@ + + + + + + + + + + + + + + + + + + + + + + + DPL Platform architecture documentation - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL Platform architecture documentation

+
    +
  • Architecture Decision Records (ADR) describes the reasoning behind key + decisions made during the design and implementation of the platforms + architecture. These documents stands apart from the remaining documentation in + that they keep a historical record, while the rest of the documentation is a + snapshot of the current system.
  • +
  • Platform Environment Architecture + gives an overview of the parts that makes up a single DPL Platform environment.
  • +
  • Performance strategy Describes the approach the + platform takes to meet performance requirements.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/architecture/performance-strategy/index.html b/DPL-Platform/architecture/performance-strategy/index.html new file mode 100644 index 0000000..0558335 --- /dev/null +++ b/DPL-Platform/architecture/performance-strategy/index.html @@ -0,0 +1,3227 @@ + + + + + + + + + + + + + + + + + + + + + + + Performance strategy - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Performance strategy

+

The DPL-CMS Drupal sites utilizes a multi-tier caching strategy. HTTP responses +are cached by Varnish and Drupal caches its various internal data-structures +in a Redis key/value store.

+

The request-path

+

The request path and expected traffic

+
    +
  1. All inbound requests are passed in to an Ingress Nginx controller which + forwards the traffic for the individual sites to their individual Varnish + instances.
  2. +
  3. Varnish serves up any static or anonymous responses it has cached from its + object-store.
  4. +
  5. If the request is cache miss the request is passed further on Nginx which + serves any requests for static assets.
  6. +
  7. If the request is for a dynamic page the request is forwarded to the Drupal- + installation hosted by PHP-FPM.
  8. +
  9. Drupal bootstraps, and produces the requested response.
      +
    • During this process it will either populate or reuse it cache which is + stored in Redis.
    • +
    • Depending on the request Drupal will execute a number of queries against + MariaDB and a search index.
    • +
    +
  10. +
+

Caching of http responses

+

Varnish will cache any http responses that fulfills the following requirements

+
    +
  • Is not associated with a php-session (ie, the user is logged in)
  • +
  • Is a 200
  • +
+

Refer the Lagoon drupal.vcl, +docs.lagoon.sh documentation on the Varnish service +and the varnish-drupal image +for the specifics on the service.

+

Refer to the caching documentation in dpl-cms +for specifics on how DPL-CMS is integrated with Varnish.

+

Redis as caching backend

+

DPL-CMS is configured to use Redis as the backend for its core cache as an +alternative to the default use of the sql-database as backend. This ensures that + a busy site does not overload the shared mariadb-server.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/architecture/platform-environment-architecture/index.html b/DPL-Platform/architecture/platform-environment-architecture/index.html new file mode 100644 index 0000000..d187d32 --- /dev/null +++ b/DPL-Platform/architecture/platform-environment-architecture/index.html @@ -0,0 +1,3521 @@ + + + + + + + + + + + + + + + + + + + + + + + A DPL Platform environment - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

A DPL Platform environment

+

A DPL Platform environment consists of a range of infrastructure components +on top of which we run a managed Kubernetes instance into with we install a +number of software product. One of these is Lagoon +which gives us a platform for hosting library sites.

+

An environment is created in two separate stages. First all required +infrastructure resources are provisioned, then a semi-automated deployment +process carried out which configures all the various software-components that +makes up an environment. Consult the relevant runbooks and the +DPL Platform Infrastructure documents for the +guides on how to perform the actual installation.

+

This document describes all the parts that makes up a platform environment +raging from the infrastructure to the sites.

+
    +
  • Azure Infrastructure describes the raw cloud infrastructure
  • +
  • Software Components describes the base software products + we install to support the platform including Lagoon
  • +
  • Sites describes how we define the individual sites on a platform and + the approach the platform takes to deployment.
  • +
+

Azure Infrastructure

+

All resources of a Platform environment is contained in a single Azure Resource +Group. The resources are provisioned via a Terraform setup +that keeps its resources in a separate resource group.

+

The overview of current platform environments along with the various urls and +a summary of its primary configurations can be found the +Current Platform environments document.

+

A platform environment uses the following Azure infrastructure resources.

+

An overview of the Azure Infrastructure

+
    +
  • A virtual Network - with a subnet, configured with access to a number of services.
  • +
  • Separate storage accounts for
  • +
  • Monitoring data (logs)
  • +
  • Lagoon files (eg. results of running user-triggered administrative actions)
  • +
  • Backups
  • +
  • Drupal site files
  • +
  • A MariaDB used to host the sites databases.
  • +
  • A Key Vault that holds administrative credentials to resources that Lagoon + needs administrative access to.
  • +
  • An Azure Kubernetes Service cluster that hosts the platform itself.
  • +
  • Two Public IPs: one for ingress one for egress.
  • +
+

The Azure Kubernetes Service in return creates its own resource group that +contains a number of resources that are automatically managed by the AKS service. +AKS also has a managed control-plane component that is mostly invisible to us. +It has a separate managed identity which we need to grant access to any +additional infrastructure-resources outside the "MC" resource-group that we +need AKS to manage.

+

Software Components

+

The Platform consists of a number of software components deployed into the +AKS cluster. The components are generally installed via Helm, +and their configuration controlled via values-files.

+

Essential configurations such as the urls for the site can be found in the wiki

+

The following sections will describe the overall role of the component and how +it integrates with other components. For more details on how the component is +configured, consult the corresponding values-file for the component found in +the individual environments configuration +folder.

+

Depiction of the support workloads in the cluster

+

Lagoon

+

Lagoon is an Open Soured Platform As A Service +created by Amazee. The platform builds on top of a +Kubernetes cluster, and provides features such as automated builds and the +hosting of a large number of sites.

+

Ingress Nginx

+

Kubernetes does not come with an Ingress Controller out of the box. An ingress- +controllers job is to accept traffic approaching the cluster, and route it via +services to pods that has requested ingress traffic.

+

We use the widely used Ingress Nginx +Ingress controller.

+

Cert Manager

+

Cert Manager allows an administrator specify +a request for a TLS certificate, eg. as a part of an Ingress, and have the +request automatically fulfilled.

+

The platform uses a cert-manager configured to handle certificate requests via +Let's Encrypt.

+

Prometheus and Alertmanager

+

Prometheus is a time series database used by the platform +to store and index runtime metrics from both the platform itself and the sites +running on the platform.

+

Prometheus is configured to scrape and ingest the following sources

+ +

Prometheus is installed via an Operator +which amongst other things allows us to configure Prometheus and Alertmanager via + ServiceMonitor and AlertmanagerConfig.

+

Alertmanager handles +the delivery of alerts produced by Prometheus.

+

Grafana

+

Grafana provides the graphical user-interface +to Prometheus and Loki. It is configured with a number of data sources via its +values-file, which connects it to Prometheus and Loki.

+

Loki and Promtail

+

Loki stores and indexes logs produced by the pods + running in AKS. Promtail +streams the logs to Loki, and Loki in turn makes the logs available to the +administrator via Grafana.

+

Sites

+

Each individual library has a Github repository that describes which sites +should exist on the platform for the library. The creation of the repository +and its contents is automated, and controlled by an entry in a sites.yaml- +file shared by all sites on the platform.

+

Consult the following runbooks to see the procedures for:

+ +

sites.yaml

+

sites.yaml is found in infrastructure/environments/<environment>/sites.yaml. +The file contains a single map, where the configuration of the +individual sites are contained under the property sites.<unique site key>, eg.

+

yaml +sites: + # Site objects are indexed by a unique key that must be a valid lagoon, and + # github project name. That is, alphanumeric and dashes. + core-test1: + name: "Core test 1" + description: "Core test site no. 1" + # releaseImageRepository and releaseImageName describes where to pull the + # container image a release from. + releaseImageRepository: ghcr.io/danskernesdigitalebibliotek + releaseImageName: dpl-cms-source + # Sites can optionally specify primary and secondary domains. + primary-domain: core-test.example.com + # Fully configured sites will have a deployment key generated by Lagoon. + deploy_key: "ssh-ed25519 <key here>" + bib-ros: + name: "Roskilde Bibliotek" + description: "Webmaster environment for Roskilde Bibliotek" + primary-domain: "www.roskildebib.dk" + # The secondary domain will redirect to the primary. + secondary-domains: ["roskildebib.dk", "www2.roskildebib.dk"] + # A series of sites that shares the same image source may choose to reuse + # properties via anchors + << : *default-release-image-source

+

Environment Site Git Repositories

+

Each platform-site is controlled via a GitHub repository. The repositories are +provisioned via Terraform. The following depicts the authorization and control- +flow in use: +Provisioning of Github repositories

+

The configuration of each repository is reconciled each time a site is created,

+

Deployment

+

Releases of DPL CMS are deployed to sites via the dpladm +tool. It consults the sites.yaml file for the environment and performs any +needed deployment.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/backup/index.html b/DPL-Platform/backup/index.html new file mode 100644 index 0000000..552a81f --- /dev/null +++ b/DPL-Platform/backup/index.html @@ -0,0 +1,3165 @@ + + + + + + + + + + + + + + + + + + + + + + + Backup - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Backup

+

Site backup configuration

+

We configure all production backups with a backup schedule that ensure that the +site is backed up at least once a day.

+

Backups executed by the k8up operator follows a backup +schedule and then uses Restic to perform the backup +itself. The backups are stored in a Azure Blob Container, see the Environment infrastructure +for a depiction of its place in the architecture.

+

The backup schedule and retention is configured via the individual sites +.lagoon.yml. The file is re-rendered from a template every time the a site is +deployed. The templates for the different site types can be found as a part +of dpladm.

+

Refer to the lagoon documentation on backups +for more general information.

+

Refer to any runbooks relevant to backups for operational instructions +on eg. retrieving a backup.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/code-guidelines/index.html b/DPL-Platform/code-guidelines/index.html new file mode 100644 index 0000000..466bed1 --- /dev/null +++ b/DPL-Platform/code-guidelines/index.html @@ -0,0 +1,3385 @@ + + + + + + + + + + + + + + + + + + + + + + + Code guidelines - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Code guidelines

+

The following guidelines describe best practices for developing code for the DPL +Platform project. The guidelines should help achieve:

+
    +
  • A stable, secure and high quality foundation for building and maintaining + the platform and its infrastructure.
  • +
  • Consistency across multiple developers participating in the project
  • +
+

Contributions to the core DPL Platform project will be reviewed by members of the +Core team. These guidelines should inform contributors about what to expect in +such a review. If a review comment cannot be traced back to one of these +guidelines it indicates that the guidelines should be updated to ensure +transparency.

+

Coding standards

+

The project follows the Drupal Coding Standards +and best practices for all parts of the project: PHP, JavaScript and CSS. This +makes the project recognizable for developers with experience from other Drupal +projects. All developers are expected to make themselves familiar with these +standards.

+

The following lists significant areas where the project either intentionally +expands or deviates from the official standards or areas which developers should +be especially aware of.

+

General

+
    +
  • The default language for all code and comments is English.
  • +
+

Shell scripts

+
    +
  • Shell-scripts must pass a shellcheck validation
  • +
+

Terraform

+
    +
  • Any Terraform HCL must be formatted to match the format required by + terraform fmt
  • +
  • Terraform configuration should be organized into submodules instantiated by + root modules.
  • +
+

Markdown

+ +

Code comments

+

Code comments which describe what an implementation does should only be used +for complex implementations usually consisting of multiple loops, conditional +statements etc.

+

Inline code comments should focus on why an unusual implementation has been +implemented the way it is. This may include references to such things as +business requirements, odd system behavior or browser inconsistencies.

+

Commit messages

+

Commit messages in the version control system help all developers understand the +current state of the code base, how it has evolved and the context of each +change. This is especially important for a project which is expected to have a +long lifetime.

+

Commit messages must follow these guidelines:

+
    +
  1. Each line must not be more than 72 characters long
  2. +
  3. The first line of your commit message (the subject) must contain a short + summary of the change. The subject should be kept around 50 characters long.
  4. +
  5. The subject must be followed by a blank line
  6. +
  7. Subsequent lines (the body) should explain what you have changed and why the + change is necessary. This provides context for other developers who have not + been part of the development process. The larger the change the more + description in the body is expected.
  8. +
  9. If the commit is a result of an issue in a public issue tracker, + platform.dandigbib.dk, then the subject must start with the issue number + followed by a colon (:). If the commit is a result of a private issue tracker + then the issue id must be kept in the commit body.
  10. +
+

When creating a pull request the pull request description should not contain any +information that is not already available in the commit messages.

+

Developers are encouraged to read How to Write a Git Commit Message +by Chris Beams.

+

Tool support

+

The project aims to automate compliance checks as much as possible using static +code analysis tools. This should make it easier for developers to check +contributions before submitting them for review and thus make the review process +easier.

+

The following tools pay a key part here:

+
    +
  1. terraform fmt for standard + Terraform formatting.
  2. +
  3. markdownlint-cli2 for + linting markdown files. The tool is configured via /.markdownlint-cli2.yaml
  4. +
  5. ShellCheck with its default configuration.
  6. +
+

In general all tools must be able to run locally. This allows developers to get +quick feedback on their work.

+

Tools which provide automated fixes are preferred. This reduces the burden of +keeping code compliant for developers.

+

Code which is to be exempt from these standards must be marked accordingly in +the codebase - usually through inline comments (markdownlint, +ShellCheck). +This must also include a human readable reasoning. This ensures that deviations +do not affect future analysis and the Core project should always pass through +static analysis.

+

If there are discrepancies between the automated checks and the standards +defined here then developers are encouraged to point this out so the automated +checks or these standards can be updated accordingly.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/diagrams/build-release-deploy.drawio b/DPL-Platform/diagrams/build-release-deploy.drawio new file mode 100644 index 0000000..eb4adb0 --- /dev/null +++ b/DPL-Platform/diagrams/build-release-deploy.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/DPL-Platform/diagrams/cluster-support-workloads.drawio b/DPL-Platform/diagrams/cluster-support-workloads.drawio new file mode 100644 index 0000000..d1c05d2 --- /dev/null +++ b/DPL-Platform/diagrams/cluster-support-workloads.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/DPL-Platform/diagrams/dpl-platform-azure.drawio b/DPL-Platform/diagrams/dpl-platform-azure.drawio new file mode 100644 index 0000000..155a004 --- /dev/null +++ b/DPL-Platform/diagrams/dpl-platform-azure.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/DPL-Platform/diagrams/github-environment-repositories.drawio b/DPL-Platform/diagrams/github-environment-repositories.drawio new file mode 100644 index 0000000..0510d2a --- /dev/null +++ b/DPL-Platform/diagrams/github-environment-repositories.drawio @@ -0,0 +1 @@ +7V1Zc+JIl/01HTHz0BVabfOIEcbyh0RhwFi8dICghMTmMWAh/fo556aEweBaumvpmaiqrgalUqnMu55780r8YdYWu8bz8GnqrcaT+R+GNt79YTp/GIauaxf4YEumWq54xIboOR4XnV4bOnE+KRq1onUbjyfro46b1Wq+iZ+OG8PVcjkJN0dtw+fnVXrc7dNqfnzXp2E0OWnohMP5aWs/Hm+mxSouKq/tt5M4mhZ3trVi3oth2bdoWE+H41V60GTW/zBrz6vVRn1b7GqTOWlXkkVdd/PO2WJe601WznS+CmcTntX/MK/L2T1PlpuvGe7i0Zm33frDgzFw3XG/3bkPan/ql2qYl+F8W9zk5K7Pq+1yLLfVcNt0Gm8mnadhyLMpJAJt081iXsxqvXlezSa11Xz1LFeb44vRhX2BM5/i+fyg/ebmpl6/Rvt4uJ7uF7VYvQxHcmMePU/WcX54vNoMNwfHkMTJ4fFkHB8eFgJz0HJKs4LIL5PnzWR30FTQsDFZLSab5wxdirNmpeB2Ie0XtjpMX0XHsgsiTg/ExrwsOg4LcY32Q78yDV8Kvn0DD039yzz8AteOeHDCwovwajL6dMrC8XBy9Sn8PmTVjS+S1SgV75Cs+tUPI6vxg8l6ZYzMizOaMbYnV2Prx5DVtLWvo2sp5d+drJUTqjofm2joTOXiNwRezyabcFqYnqdVvNzIfOxr/IcZ1tQ/G11rbPlg2Gcaz7Vdnjbqp93woZ+7w9vGc22Xp436aTcelbM+bjzXdmmfzvjt1fqZq/U3V+M/83q13czjJSSydKuk8afVcnMgi/h7Q+ZeR8/DcQw5dOJndI5XS5xfrp4pQ2/lV9MsR9POSbwmfw4VgweU5hjuuDkcTeYfV+u4GH602mxWi4MO1Xkc8cRmRT0bFkchZjV5PlY8rqIAGrpRHhdSxVsO109qyZ/iHedxDdf9xJOLXUSQ82GYrq0PcD+r7XM4cUPOh95IfTvuFa4Wi+Fy/Bdp+VfMuXwSe7An2QEBKlc3pm1+H9W+qHyoHP05VnTtVNHtM3pu/yg9P3VKjXhzux2hbfUcDZfxeih8PtH4ghPxQmDbu9KxiMdjXnM954nrYTiLBKgcUPuT/DkjQPtrT+RA7lotW7WyBd+nmw3xaJVUMW62T/PVcPwhjWfxAqhj+AFrQjOPn3iM75SL1XKNb5vpdoF131T4D3S5aUGDIEnrPxfD59mfUYwOow/rF44Ag6097f58r8eHp2VUrvmAGM+KoXudmE8+bb4T3LG1D1BovWLa1pV5dakfSZlhQQYv9n8u7TNg6IzQWT9K6Eztyz77awVsb37+hoAVFurrpWsMXAvhUofGjcjC9Q72zKh9vPWNQXZtjfq7bZg/zYJci4e391rorF6a5rUeLtLtyLxbNo37pGk8rAd9fT5a3ufNvL71OlexezvdjBp23lr4ycfO3Wp8e5+24quXwLybB4/3T+PFQzIy9M3IsPPmopINsso2zLzX65Z3s0FyeM+xOc5s08vsl3ARvnjdmd3qXKVefIWr9GzQCDahOd+OGzdWs2/nbuZGk4a+Hi29i9AcLA/ngJHM5jIs7ovrnWraNLne/TUVdzHVxrfVi2ZWQe9wO849td7cTdH/hWO68Z4++ci4fwobldmwezhn/2XQmKc811z6L+PHu2TQH2D+43lzYc/HtUr9od5+CQ1c93iNvrONV7Nn40b7gH7z7dD0k+Dxen6yhoNzJQ0D8CGEVwqMh7xpvJ7HXM1h/14bOlrsd91tq1s3/GSwGXXcaNh4eBoYU+1jx935HSvznbbtJTdybryYz8fa3csE13m1auo6vayZzHYY1XCdut5MXM13ZpknfR+y0Ji/jMA3r2PtcM+nAa4jrVo13KdvP4eGPw0bvYujvjXL9mL0v8X9u57JsQ5ocjFoVJLR4mYzAG3bj0+QvQcb9CS/IK93sW8F3XXkOrsZqJu7jfkMlJLvkKeXj3GQTBr1S7dWvTocVUbs3xvD/oNZXBMHjz4plQb98TwwKuuR6Vbc2F8M5G8Qu407cO9+Hi78l1GjkoFiCSkh1IivY0jIbPCIPo2bZPh4b5OizaSK61450VpeTwfGw2Gf9J0++bBxMxuZYcVN3Dfnn15Ghob2YAtuan5SNfzsmMKBGfG82Uzaht+dHV+/8HnfvFWzUi9p517ivTl/D019AH1srfn4sMU8c9BHScvRPcDFxcN2XNPXweN8HoKbo8aNPXh0yX3Mv2IOzDuRgsHiZh0avXPrTCCBydB4yIJFZRou2hV3eZ+N++w7eMLYm+DxbjnsW5txo5KSXhh7MXxsb0b9m2wAKW/2d/PBEtcJHY+vCRfz5fD2/DncT8OYm2LMp9FikwdwqIPuE7TFJu03Y2MOjSQtZ7n3Vopxb0in2exP58P+eDWmduXR7r2xqEVYufnueUi4162/ex7rtYZ9/WmyeJipOV8da8Bm8Hg/HTRutKCjdG/Uf9CC/v103KjrbmItXHM6bWXVSLRhcb9oze/q98ftHCcJ8qdHSPOze0s7/DQFpXSuzn3bF/8+Nu6T1oJ2TZ+OHfsJs1wOOpV8vAifaU8/1irwH08LjAfKtw9nXNvfkT20CS2VumM2eCwsZvdznuc+GfXncl1T97Vhf7fu9HXIVC9u5u/MdAnv09jNT8476UtgeBcPRiUbHp+7apoyp859PTiid/pyQN9jTiyP9WkHCj6soUOe6ElcfSsjyxAUDhs6bFMP3mc3nfQfstKCHuiUFi5utqFBamuiJwG8Hc5rg05k+HkIy+7tvCzdeHk9byaR7jtV8z8d9w3VPzbCaHx7Nx0tfeELtRt+ORs+RiuPViGfmS0H9jhL06ZTXb9p2/hJz2omvZ3vRFbT8dBm5X4CnxKLpaE1LSxCfeO9sRqYbTzopxV3VqzSqccHVlrN7/YaNI2OrY1R2Y6AG6CxWbPv6+FyMB3fPmSUEKWZtBxynfgjoVynuvGSnt1M6pafuGbTcU2/Zul+jrnH14nrBKBSz/Dy3rrZrcNau1kLbZgTzlupn3jwIqBmUqWlBQ3cCBpqNMXqu3azpmWgjQE67Fqd6zWu23rODOO5GK+6hWXNfMwBFhrtdVjbOtpD3MezPMc13IYXNbse5lBN/W6P4+Ut+uAufOzhdd2ouK4Ha9223ZpmCE+61V2rBn4Ad+GfBf8ZeTjGuRzzzbysasAX2z7X2EF7N4S3mcG/1zOf52Ir97qe7cdyjuPbLawT59APNEjaJuaE71h7MjNdpw3/7+I+M73phPgeYA5ty+UcYyvDuDrot8ZYoFHb8pLQxlg6aLnzuriuluKY6+vpWN8ONNh6eah7PI7THSUY+CP3inMtp4pzbVudi7bAfRrWFY1qnF9ogS5akzSHHoEuoGXdxtpN3D9vdXuGmnsVmCY4OifrVd5PF153IBNZwWfgGj+7vnAbWuTnHnkFuY/Im53MHXz3OpAd8B68gkxUbdG4jgUZq2etGvjm9IixoH1tyhV0xSPNNVyjkzctZwY5BN+cGebuaV7icn5cXyEjEWVn51F+gaVa3bbJOcNjA2/Bo8fSDt4FpBvkpA5aRSloRNpwbaB3aLXAc78rsmHiHkA6EeSXdAVv8vbr94yyC1mDPFHWfMquA4TgRCZllLTCerKm0wbmq4L/WIv0b1u+46r+ebiD1WFf6Bf4FKe6l+G7A3rB4oFfGqwT5AbjJBH4EEBueqIjkPE11k0dwzoxB6yZtASNIKeWCSsGhFPVOAb6wOa0Qd8QfKU+8t5V8AHzziyr5Xg2eGqKnIO3TdAY/dCOvpkFOYHc0K4lIXQ9wjpoKWdai59YE3ieQm92hZwaoCFliG0YE/Lchc2DLOA78GqA+7dBq7ZGOwE5gtxG5BvsAdbeDcReco5Yj9nqziLoAu7rasIb+Y71cs3EwTnuK3raU7KAeWDOlH2b/WHLQK9Ah2yXOkBa57QblDHMzRC+YF0iX7yv0wbvCx5ink0lg5AjRFzdMBrW1D2oFzKP4rvfucZYIWynl2IMDfQ3W5Q5fq9dryDLmLdlYy2IwSBzElfA9kKWx8Ux5gMe0s5WRaZ96DLkPRLb6MxyRRv261HuVHsy4xjUm4w2hHaQ8kyUW/BCh75L3xbtOXQM8gLZ96C3ohfkJXQkpa0yBHHVNMgO6JAj7nNoR3pWqys2AXycZX7eplxCtgPIokv7mimZ76Wwm5nSp56OdpsygD6wO+BFHtHGiW2hLWw58ItOSHksdFpse4Y1474BeaFhvdEkvt64uI9P3wG0VdijnZdEyi+RtjXGYpC3jpwz/byay7zE5kSa6D36UKda4udmoEcPMhaAdpD5HLR2SFPaH6zXCSknO6FPN4JuqmvAI+AU2hwPtG3rtKm4D2TBgo+CTREbwkjD0yi/yk7VIT91uQ9oQX+q2mnzxZcy8qmjf8CxMK63a4kthO5ija1uFb4Ett8JTfoLXAvdqgNDRMADEdZRR3+OQ/rNIO8h401c26bPo13XfPFrMwuyRznRqVNKD6q01znH8RzOuadkL4GPoX9U88e9Sp9fh90KdtIfqAn6hHasV+x+aKr7VmEL2yX9IC/w8dAd8Iu+B9wVO08sgX6e+FZcA/sQ2spWeJRf4gpD5AS2T7UjqoCtBC0EA3icj+oPOenZqj/8J/0p/Bzxluf0IsgX+At7CxrRB0FODOiJpuQEMp+JzNrwQ+S7spG0RZ2UOpvS7vm0cR3a1sCijMO3QJeBQ2ADYJcYVQIHpLBTHHNmlZjA45ocb0tcBdmNRG6hB9BN0GnG8QobBGyQyf3gQ1Kl29220gPoobID8NGJm9MmN+WakLZQ5E2wE2w69RD+K3Nvca/Y0mE3NOWTUshMkNPPEndR9mk3PF6be8Ax9cgnbRLRIWAhrtUlnSAvoUHeci6YO3juRbD3sCu0NyltIPBT2/CUL9Lo22BPSRfcIwLWqVqKX8AHGIP5FEVn4BfYY4WBiAUxJ6cucgJbDNvHdsp9Lxf/m4FHwBjAcNQRA36+8Ccz8G2mMKRTt4mVwB/4rsAobAX8Z4/YWfIx4FtWyJPFjADkCbasDhq0qSum2My4mot/6sKHio9pU54g46Cl+NvAainsaPmkg+iVS3021dgBMIybU2+Jd3Efyjx1lfiZcgBfNjOKMXZig8VHwT45ddhLl/hCo01S+LRKuSN9sRb6EGAQhdfSlmoHvUJbfEsMO4l5U54wD/ADtliws2B22JOw4ANwFvgOO6PRDkBPaNd34hsxZ9o18NlgHOMpPqeMC+g3fMoZbZD4GGBO4FLaYeoFcLBWYAZiMeUfgDWI6eD3ZY2w7Tv3dhUh5tBa4svBF6EzaRCQL1Yht8rn0YY4tKf0I/BBYu9c8mrHGEJ8QEaM6ik/mbcp16nEBR36754lsViHuNszxPeJbLl2iSF98sbxCv+EaBAy0xQfFhCDZK7YyxD3CDSFlSGvypYzFmD8s1Ptno4+7E+boXFNvshsm3Ef/Sv0wUUfT/A4fa6vfI+u7DFxGLHLrPDdkhkDfu1pij91iS0Z14jPhq0S3UQcpOxEnXEL4yrYD9rZkPFEVtAIPA+JgzPGIKBr2lK4mbzNMU/aVtjNGcfPFJ5h3Eoc4BEjcj6Y8wwxGPUTuAJ0grztsTR0hPhb2WWnZxBH+7SB+A57c/B9Rt/uA9fmio/ws5gX/A7WzhgvigKxo8Cz8LuKttBvR3wD41SMgeMCu4EX1EPIGG1QOy18pdgkP4fdps5ITAb/Al8nsaryaRnH9BV+JF4k5mX8oA86WoF5e8SLSk4aPch/Va5jzNWU2Iu4SOaLvsB8iGlbtA3wEYhxEMtRb6uMj4EjiT2Yv0W8AN8i/IEfxDjEwdCFOnWUsp+KXgo2IqbAOMALyofAx5JvYpcCxkiqnXgwE9sAG0y/5ElcI/GbU9oA3A+xO/2/xEvA+wqTSuxT4EnGcnWLGIt40CeuBpYXfAMbJHQ5bFd43ijaKesp8a3Em9A9ymWQiXxgfPh9WRvaIWuM8ySX0FVyQ5wHWcsFywr+4D4CfUcxJwe6JT6ynirawR520lyt3838PV1mtJ+m8pXVAxtIjB+qPI0jcTDxLvxJPS1pBWxlFDSEv2sTdyj/krehe+1CbuugLe3aDHgA806IZWE3ENeJ3Cr7rNFPNeHrWsx7OGGB99qIU+q2q+JkxvSZ2CeHMX1EfhZYFHg4ESxFO8kcWUbZUDYK8uQEO4lR6BecKuMU6qdgU9g8iV8YW9GWwEYLrhB7kwuGxXeRBYVHHWKnQLUTc6gcAeYDf+pEsn5ggnwfkyRRDj3JlL9ok4+0g7A1gUlcD1mlnVb2gvY3UfYCuqGpdo4X2Bxb6SgwBHhHvSM+EVwmOGRmKvkUjAqb6K2ZC4A+0N6bKs5oE7vSb+qM8yBzWUFb+nvGg4gBoFtJaCrbD151I8jobE2aE3/B3hc6VyfGKuJiyCHiLZEh8oz+Pyc+cQXHqrgDfi2rZkqXVLzsOh7jCBXLFHJAWXu91ivwCmPVIn/C+CKvCpZpKf8PGz6T+LDwt5ARsWGGYBfFe8MXekEG4Ut9iTHbkr+CH2a8YjN28YlDmLsSXyfYcqd8P+0h9LEbFL4La4GOFNjCVHIvemYXsSVjR8HpxFX07y0VFzN3pku+oPP6XXBoxpxfQKycE4/4xOjKRqn5dlKuwWYuCbZLckU+80PMn4DfkjuAXh31Z1xHf03aCJYBHxnvO+R98Go7iRsTxr7MERa8zYkFScO25NaEt7Q3iDWxDt4D8ShzbIzpufbIYL+D7+LTYJN1kQ+nTRnknCgTwIuKn/yu8m5tiQEYUxa5MM4DcWnbbr5+N4p2i1iQtpkxm+DPuPSrjHeZv5Q4lLFw9BBXGRfnjEWYt5D8REza1VWepPwu+QnGR8DN8Od+pvIMkpNjvqcj8UrK3FnztZ/tKrmnnhPPM+Ys++aMBUC7SOIo+ADJmYoe41rxj/A99G+Cm5h7YP6IvpL5rDJv10M8z1xRRAwoMaOKVXvUeWV/HcGTiL+J12aMaRirgK5Vu/BHkLU6Ma/k6GBbMhVXMzYGpqGNcGgTmdeaWYXsAh8wpxVGkouFj/f3ueC2rfJnwB2qjfkJEz7fkh1C7hQqnIcxqgbzcpAd2BaxB7AR7SLmaXO/XGwHxrSId5SPjVSs1qFehPCFZS5c/ORa5uAAb9SEZ8xVMVYo8QlsE3Va9CcXH5BHRdzcg4wElvJzxMk9WzBOLBi+wLy0//B/3XZh50HbRHyNKTGaGosYZucpTK9JriGvKj+aQEe7nuAR2OiMcY3w32FeRo3vERd2aIdCyb36khvpEd/T1sB+z+wiH4a19BjP8d7Eg+JDmP8m3qZPxXqzIpekC17IQ41YtMV4SPwgdSHAtZhHUmDAPQZGDAR/y/gfMie8Vn6rnjJvwRw02hl3Cd9xbSp+nLnMnDlJ1Q57qov/EGzoZYXtKuxLHf6jzdy74L8O7aD4lh7xZqIwuvheykGk8nQh47ky324pny153pw0bR7YAcZIXcYwxODM6SHuAc3YF2sgDtrvYewE4zjMGUFWExmH+QjKBHOVhuSo8sBUextVW2IN5pyApYD9sQbGRXKOeSfmiyhzpvCAMtdRuXPIJudJjKOJ/8vVXgx9NewwfbgpcgoMofa/eA30ltg/95TPl3g4oMxq4J+p8uMe7XHhF3upX9KfuWuJZV1iLVvhjDowDHws7Uk32GMS8IB5IdGtFjE9eZsTOwes1VhzX4Q5duiwTf4qWwo/kszWar+nx11AQ+HLkDS3mU+mTQT/mQu0lBwxF8PcJmO4Mhcj/CamzmjPFcYFTu+6x/0V32W/QnKFiMFUTow0qO+Yp5/EbhSa9/ZIdjgHT6NGeuHmiCWc+cxPNgvu3jb7lTTo+0/j29mF193k49u7l6HRK3fhL1SOwSWe5x7fy9G+oZO+DA52bj/WKsvyPL5zP/oP4/r7VKgZ1nFJ2mkJ2v4JlaN6/O9Qg5Y+xN5oNcou/6cdvfzlbP7jfuz+aZ6UoNVWyw3X940PbJwd/bSebTKOJmVtKwtyV9FqOZzXX1uvjx/geO3TXLFMTQpmk8lmkxUVs8PtZnVcTjtZjqt80IbVtvPheh2HqvEmnpdd1LQ4lyM2Hq72c9RShbafKyctOm6Gz9HkcwO+IyzPk/lwE78cz+671x9a9q9gz2HN9njyabidb6T6evi8Kdm2XC0nZVvBNe3rGHtcxP06/E/g73kSW2dF7R9wXC4FGYbZQYfiWYPXkT+y4aD62j42O/rFxRvpUSO+ytJ+av+gvNX+cnnrqzTpX35a6zMPqLxy+rC8v5Cjbytt/6yZ/PpHWS7MY4prl6eWXj/3iJDxo8qNv+IJof/b1cafqfolmleevKwy8rra+CNi3+NK1VyyOqzUxb+QSAiIRSo9bJVtY7uXAiUhykNE73iC3CXidNqWZA+7LvoHzJoTAQIxudwdQtQ+Mw+raYGWM1XNU5WsMrOLQLOY30FNoOzMhkCIM1OiQ4eVAGH8MVE1q/gE0pmvB11rGxi7vNm9uRws5uuRs0p8w9W8xV3SMgQxbcLlA/o9zceLh+3IuJ+1TO2qafrapL+bf0yAgBoPizC3rsLGjTasXZNGvqd2XS1VvXHvehIZBflDwoxOYA0d7vzWs4ekPO8B2T30/Xy+YpbPT+6GqtawrMtibVWX9S3M0XVSYmPdM+q5xE1ZWn4WtWOpLbm4fL6WfaZOyrjHaEs+NtBVDpSf0nfHfXj0Ya2B3mp4G6mHYp5D1QMYTe6PZRJraX4s+7F56yZgPZQW5BKbZSqHB6yrcqO2qjlxs/I4zGV/w+CehK/2mqT+SGp4HgOs3gcVeulYalJuHH5X9U7cx6rrgoezYt3MIyA2Zf5RYoGuPyRtsBbmjHaeEbGGwnqQOrTA5By5J9qqaYw3N5JH70h9gsW1+flNzk/ugXF/uPguOfU+K5qTXRo83q/cRrvizjTGQsy5rWUOyXQq+DsJ1uWn5I0c0IX7VTKOv+esl0/7Ku94sxK6GSnjmjXjI5/85L5bnJafip8dxraIWbvM/4zPSoabYlXc8WFFhRV0e6aiXPlZVbNiBrn2NTOKDmdk+p20/Dyckf7ZGXF/22Rc3upHG6nt66cbxmrN7t1U9owRp+0/hWb1TUvJMfg+SIp9Opzjfiosg+SaZxvZT+xw/4v7yZQ/9kccKLmg2Vpyv3LetYeSl8UKGeM7fuKpPJLR7Fi28B735J6DF4s87IJuW3L70Nzis6Qc965APVnDbNcU3mMdrJHL3fX+U61DctSQMdkHDRAfekouoEOpuj/1Q3KBgdafaZnUYkF+KTN+Pcq8LtfPPBTHlByNyDJp4FFemIcWbiAGZrvjgWNpHoi+BZba4+NnMX/JLwUbyak+rFite2yxkl65btByOuV8JAdTfpYyzVgdeuQ7c5mf1AdxTlw77UYmdVEZc5WQKIN5W/Af/Hvw5B4ioZ41VHvk5gPiWfVd9LU8R92jdbSZjxo61NG7Pmv8/AYsp8hMfS15ScnbU8JZ1xfSdu38Rp05f+6pcc9tzxPJPdLenF0/87AR+ML6q57VhveRWo24sIs12VOEvWkzd8KaCsw3YKyfFTaQdTu0gYXN436q5GP2x2Gu8jMFL9aqFtCTugU/uenDljhSl0FZd5ijco/vn/S23I9U+YbpcNRlPRH3qKeJaGKXeT/muNpqTzjvqX3rWio1hA+Uh+QOOkAZZH0UbS3o0rG4Z1Vcx/z3PJfrRH5DZbMdGSttPYjdz1p11orQx+D6bgiaiOcWmyj7r+VceTw/R++ZqeTzmyyW65Ge9JTvWSxNLBb3c8vP0mJxz2vtS97v5qzFkqoQEyvUW4VnEy+VadzhogfJ4f2IPQxKvpcP4Cl6UgkHa1Oshho7P7Ct89eZ0spwpszOwpO2IKXCWc5MfZYzlQoNjCm76EFOKREpBhWCQ49uSlVQ934qmSf+21sfVolA27mz2vBwnZwzCu+/lSwWPYxUEIW7N1qItntayp3cgxnm7HDdgaLFos6dZu6Al58F+jihxSu1X0a38+XIsCKgrmlo8rkvfx0CW/ApoJEx345v0Sa2KMhcwSlt23VCqR1V++7cdymPA4266jplf9YAcI/lzfXfeHeijmh3cLfiuLzbTCoYX+9WHu9ne3z9N95dagKzEjm7TnlMf8Q6I7GJmXouzDVfj9nP3b32L67/m5SXrGX+SmkiKu5S7CkvWnp4fMAFvSmV62/GUjP5fF7wOwWRxtWHq4vjJ6UvT5+U1i+vPhhXP/HBVd06CSWbw+0ynP6adB+W5Oz2OSIeZYdHHyfPMdbNp+6l8Tvngi6+MhVU+d6ZoH/EwosTDlbHi3gZrzfPww1ieONCsikjfov47b+m28Vw+d8nHP5iiuCAVV/5aoTTV7pULu1L5/TdDVWz6lw7516IkG+fJx+2axnuR2lnmT8pMzxnXlZi/lStPM23dTq3aPjPhLOmqTK0j9XuKW8/DtdrqCDfZIX/dSfPz8NPq+fFr9Dmr8sDg1yPPPhgl4fBkf6/Yw128ebgMhwFpQ3B99eLeFBe852NRZkT/aK1MP5V1kI/Yy5CJpx5q18tJa97AYeC8mYnqBCl192CvysQPPjBHuWrdxe+u5D8rc0Fq9xHykoDp70RuB+wuaCfvjtDmbkH2Qf4v/Kelu+d296/BWO65hOwn3l7xY656aap+h2+kQLjfO1bLJJR4ybn3Mrnje+X0+6kr8cjgxEvq9ndKFxU1nzSNry9m4fGA9+0oQ37lS2ukXcotBcVS55JZ/VUwgq1Np+353sE8qB/J9eW7wDY78o3wtfnkq/Gjbk2avT2e/plNPwaIeh52KhkuPN6sIw2nH2zz2ck3Uhly7ECY5AM5Nld9fxvtyPPbzDzH7Uz1oD0ItbyuYw65TPA57WDz6jVYc0Wn+erR3zmxZWcVRh1a8V1XY/14Ds1hpUX9boch5EW++28Gp8bVLXKRT+d/VhLiSiM1zMKU/WjrMVTUS7rTnY8j/EieS4rthhrscZC6m5Zg6Fq13tSB8UaENaIqRqidtSX58nqkV+7zlUdjxt5RQ2Q1KHEh+1WVjyTpb7XI1lzUeuelf2Ojz3W5UldzZs+ck7qPOLrFe+lnoPmcfWgj9w3d2+CCPQeHr25oMzE7OVw9OinkFMt6O8Wk5pOWd4EeZ3vaHjz3gyRMe6LqPd0JNMEch0XmXFWzsm/sVTjyUxYBbpjBadUn9eup1Lp1qnqwi15w0ldcYGrvFm9vkcgL7Pu/jp49OV9GCGsR7i8n44aA6v5SD3Xcf867r+vYuF7J3CfIwlV7xM5ldDUrZP7/C7SqXSJEgXJJJdaMl85l7/Ot1peE/UP3nfytdpzNDdQOljUUz5NX86NFUK8Z0s9xbn15OkFeeqLTxHI076s9IPUY24zzs0+pCWfmlUVXaQxq4a4e6P6SKVb0b8HDVHVyaxelCpPaI5ool6Mq9bakacgzUISDaVpYdTuyBO0RUXrAW06xVh1T763RCPAc1Y3ZyXtXUgmzlNKmVnnuf0cPKVJWXmvSFUK1iT7bKgnX8Jj2r9XefT2FXFvXp313hu2flhiwjx+UeOZpMRPfZOWfvr+tmoYTtZEqF3Ekqcvbjvx0Qfo8vjNjc65ELXEEW/5Uvr1eMHXo81jvkhtsQ6HdO7AJ/LatC+9adOsVCrh2zdtFvj1LaP3E3oP0KgQ+y1U2QfnPyw21q4+XB6Hx+aZxNXVhx9U63Y++Lo8EYOfEq8esftLMcmviiXMi39VxFlmTn42r77EnvfKjo54/Kt4WJrBfwkPrdMYrfY8GW5oDOvjGFHajcN3Yk9+eQrh/0/Bofnd08x/KydgW8cAQS/ekP1jcwKnxcwHSW2u9STzuX/LK+ACJOo0cfB5v3r0wufDGtfjPHW9enEt784+8fXvJhP2HvoEepx7r/Tb1y9/0NVrlk/e1fyPXjmtfbi6PDvsuZc4v3a+fHvizLumz7+UulzFm1dBCwUmz/WXiSKEfmYHIIzX4eqv9fDThK88fiLjyjcjj6LixcgQqfhpLdbk5GXJB9ePJ2uwoNxNeBeB/SgoZb15KbpuCbHeQKmf+7bkqxNF+2rF+W77QoXNPnmd93X9xnh3SyiKN389T0QcVrQ1/8Rvfz0L7Tfh0v43OH4ZA0/fa/+bgZ9jYOXy38VA4xRa/WbgZxh4+W/TQOM0Y/GbgZ/7/YA3z1P8egYavxn4j3zglfWLGXgaLfxm4Lf4wF/OwNMqtN8M/BYT+ssZ+BUPCP5m4GdAzC9n4GlZ0G8GfoMPNK2rX8zAr/hBxd8MfN8H/noG/s7E/CMf+OsZ+DsT84984C9noHnqAw+qrt9w8nfVwc+uOqicpgl+dtXBqYbXly/x82q5kNfsaPelEsWT02LqX/6CjL/J+xPZ/enCsP/l62wvHCeiYJ77Mdn9L8x+/w3x04i1Jb8umRe/LqmtJ5tNvIxO5eD0p2VPN8DUz3mf/GLpu793fd7s2xr//gCtP/sLrqee6Y2zWX36FIfcxFuGk6fN+kNJob/Uib/MC/uP914q9MNQxBsnVDFOJMs69+TP1T+Xq/ffInUkVt3JcMHiN/n9T+3j5HkRr9ex/MDnb8E6EixuDK/l/39tQLS91JxIw9cK0vvQ5W1NhKWfiM3FuarJyjeLDQ6fV6vNYXkF1+ytxhP2+F8= \ No newline at end of file diff --git a/DPL-Platform/diagrams/profiles.drawio b/DPL-Platform/diagrams/profiles.drawio new file mode 100644 index 0000000..9ed2435 --- /dev/null +++ b/DPL-Platform/diagrams/profiles.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/DPL-Platform/diagrams/render-png/build-release-deploy.png b/DPL-Platform/diagrams/render-png/build-release-deploy.png new file mode 100644 index 0000000..c508816 Binary files /dev/null and b/DPL-Platform/diagrams/render-png/build-release-deploy.png differ diff --git a/DPL-Platform/diagrams/render-png/cluster-support-workloads.png b/DPL-Platform/diagrams/render-png/cluster-support-workloads.png new file mode 100644 index 0000000..cf0cf31 Binary files /dev/null and b/DPL-Platform/diagrams/render-png/cluster-support-workloads.png differ diff --git a/DPL-Platform/diagrams/render-png/dpl-platform-azure.png b/DPL-Platform/diagrams/render-png/dpl-platform-azure.png new file mode 100644 index 0000000..b76b9e6 Binary files /dev/null and b/DPL-Platform/diagrams/render-png/dpl-platform-azure.png differ diff --git a/DPL-Platform/diagrams/render-png/github-environment-repositories.png b/DPL-Platform/diagrams/render-png/github-environment-repositories.png new file mode 100644 index 0000000..0742d53 Binary files /dev/null and b/DPL-Platform/diagrams/render-png/github-environment-repositories.png differ diff --git a/DPL-Platform/diagrams/render-png/profiles.png b/DPL-Platform/diagrams/render-png/profiles.png new file mode 100644 index 0000000..989eabb Binary files /dev/null and b/DPL-Platform/diagrams/render-png/profiles.png differ diff --git a/DPL-Platform/diagrams/render-png/request-path.png b/DPL-Platform/diagrams/render-png/request-path.png new file mode 100644 index 0000000..2c1c84c Binary files /dev/null and b/DPL-Platform/diagrams/render-png/request-path.png differ diff --git a/DPL-Platform/diagrams/render-png/terraform_overview.png b/DPL-Platform/diagrams/render-png/terraform_overview.png new file mode 100644 index 0000000..203123e Binary files /dev/null and b/DPL-Platform/diagrams/render-png/terraform_overview.png differ diff --git a/DPL-Platform/diagrams/render-svg/build-release-deploy.svg b/DPL-Platform/diagrams/render-svg/build-release-deploy.svg new file mode 100644 index 0000000..76f049f --- /dev/null +++ b/DPL-Platform/diagrams/render-svg/build-release-deploy.svg @@ -0,0 +1,2 @@ + +
Goal:
Create
 core release 1.2.3 of dpl-cms
and deploy it to a site.
Goal:...
dpl_cms
dpl_...
Action: The developer tags the point (sha) in git history that should be released. Eg. uses the tag: "dpl-cms-source_1.2.3"
Action: The developer tag...
Github action produces a source docker image as a package in a Github registry
Github action produces a so...
Step 1 - trigger build of release
Step 1 - trigger build of rel...
dpl_cms
dpl_...
operator
oper...
Action: The operator updates the sites configuration in sites.yaml and synchronizes the site. sites.yaml tells the sync tool
  • Which version that is being released (1.2.3)
  • Which lagoon environment/branch that should be deployed
  • Either:
    • Forked releae image registry/name
    • Default release image registry/nae
Action: The operator updates the sites configuration in site...
Step 3 - deploying site
Step 3 - deploying site
Action: An operator pulls down the deployment tools by cloning the dpl_platform repository locally
Action: An operator pulls...
Step 2 - get deploy tool
Step 2 - get deploy tool
An environment repository is cloned based on the argument given to the deploy command
An environment reposit...
Clone environment repoRender tags into dockerfile
References to the core source and dockerfiles references gets updated inside of the dockerfiles which resides in the lagoon directory in the environment repo
References to the core s...
Example cli.dockerfile:
Example cli.dockerfile:
FROM default-registry/dpl-cms-source:1.2.3 AS release
FROM uselagoon/php-7.4-cli-drupal:<lagoon_version>
COPY release:/app ./app
FROM default-registry/dpl-cms-source:1.2.3 AS release...
git clone <site> -b <environment>
git clone <site> -b <environment>
Push changes to environment
The changes are pushed to the environment repo which triggers the deployment procedure in Lagoon.

The changes are pushed t...
git push origin <environment>
git push origin <environment>
dpl_platform
dpl_...
vi sites.yaml
task site:sync
vi sites....
git clone...
dplsh
git clone....
developer
deve...
operator
oper...
Lagoons spins up a new site replacing the old one.
Lagoons spins u...
Lagoon updates site

Biblo

Lorem ipsum dolor sit amet, consectetur adipisicing elit...

Biblo...
The site has been updated
and the end user can see the result of it
The site has been updated...
end user
end...
default-registry/dpl-cms-source:1.2.3
default-registry/dpl-cms-source:1.2.3
dpl_cms
dpl...
Goal:
Create
 forked release 3.2.1 of
dpl-cms deploy it to a site.
Goal:...
"Step 0" - fork repository
"Step 0" - fork repository
dpl_cms
dpl...
forked_cms
for...
Fork
Fork
Action: Initial setup of the repository by forking dpl_cms and eg. adjusting release action configuration
Action: Initial setup of the repository by fo...
Create
Create
Configure
Configure
developer
deve...
Step 1 - trigger build of release
Step 1 - trigger build of rel...
forked-registry/forked-cms-source:3.2.1
forked-registry/forked-cms-source:3....
forked_cms
forked_cms
developer
de...
Action: 
Same release-
procedure as dpl_cms.

But, the source-image is pushed to a different registry
Action:...
FROM forked-registry/forked-cms-source:3.2.1 AS release
FROM uselagoon/php-7.4-cli-drupal:<lagoon_version>
COPY release:/app ./app
FROM forked-registry/forked-cms-source:3.2.1 AS release...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/DPL-Platform/diagrams/render-svg/cluster-support-workloads.svg b/DPL-Platform/diagrams/render-svg/cluster-support-workloads.svg new file mode 100644 index 0000000..63500a1 --- /dev/null +++ b/DPL-Platform/diagrams/render-svg/cluster-support-workloads.svg @@ -0,0 +1,2 @@ + +
Site
Site
Azure Kubernetes Service (AKS) Cluster
Forwards
To
Forwards...
Ingests logs
via Promtail
Ingests logs...
Azure
Load Balancer
Azure...
Fetches
Fetches
Scrapes
Metrics
Scrapes...
Scrapes
Endpoint
Scrapes...
Queries
Queries
Configures
Configures
Configures
Configures
Ingress
HTTPS  Certificate
HTTPS  Certi...
Browses
Browses
Administrator
Admini...
Inbound traffic
from users
Inbound traffic...
Internet
Internet
Ingests logs
via Promtail
Ingests logs...
Container
Container
Queries
Queries
Alerts via
Alertmanager
Alerts via...
IngressNginxServiceMonitors
Informs of
metrics to
scrape
Informs of...
DPL Platform
Cluster Workloads
Updated 2021-10-07
DPL Platform...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/DPL-Platform/diagrams/render-svg/dpl-platform-azure.svg b/DPL-Platform/diagrams/render-svg/dpl-platform-azure.svg new file mode 100644 index 0000000..5ddca0d --- /dev/null +++ b/DPL-Platform/diagrams/render-svg/dpl-platform-azure.svg @@ -0,0 +1,2 @@ + +
Resource Group
rg-tfstate-alpha
Resourc...
Resource Group
rg-env-<environment>
Resourc...
Storage Accountst<setup-name><increment>Blob"state"Key Vaultkv-<setup-name><increment>
Grants access
Grants access
Storage Account Key
Stor...
Terraform setup
Terraform setup
Virtual Networkvnet-aksPublic IPpip-aks-egressPublic IPpip-aks-ingress
DNS Record
*.<environment>.dpl.reload.dk
DNS Rec...
Creates
Creates
Accesses
Accesses
Kubernetes Clusteraks-<environment>-01Key Vaultkv-<env>-03-kvStorage Account: site filesst<environment>
DPL Platform
Azure Infrastructure
Updated 2021-11-15
DPL Platform...
Subnetsubnet-aks
Filters
Access
Filters...
ServiceEndpoints- Microsoft.SQL- Microsoft.ContainerRegistry- Microsoft.Storage
MC-rg-env-<environment>_aks...
MC-rg-e...
Uses
Uses
Forwards
traffic
Forwards...
Load balancer
kubernetes
Load ba...
RBAC
NetworkContributor
RBAC...
Managed Identityaks-<environment>-01-agentpool
Runs as
Runs as
Virtual Machine ScaleSet
(pr kubernetes node pool)
Virtual...
Network Security Group
Networ...
Access
Access
Internet
Internet
 
 
Managed by Azure
Not visible to us
Managed by Azure...
RBAC
Virtual Machine Contributor + 
Storage Account Contributor
RBAC...
Managed Identity"control-plane"aks-<environment>-01
Kubernetes Control Plane
Kubernet...
Service Endpoint Policykkwebhostingfile<env>01st-service-endpoint-policy
Inserts Storage + SQL
Credentials
Inserts Storage + SQL...
Provisions
Provisions
Inserts secrets
Inserts secrets
Deployment Script
Deployment...
Platform Environment
Platform Environment
Machine Created AKS Resources
Machine Created AKS Resources
Storage Account: monitoringst<environment>monStorage Account: Lagoon filesst<environment>lagfilStorage Account: Backupst<environment>backupMariaDBdb-<environment>Blob containerloggingFiles sharekubernetes-dyn*Blob containerlagoon-filesBlob containerloggingBlob containerharborStorage Account: Backupst<environment>harbor
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/DPL-Platform/diagrams/render-svg/github-environment-repositories.svg b/DPL-Platform/diagrams/render-svg/github-environment-repositories.svg new file mode 100644 index 0000000..cb56b9c --- /dev/null +++ b/DPL-Platform/diagrams/render-svg/github-environment-repositories.svg @@ -0,0 +1,2 @@ + +
DPL Shell
DPL Shell
GitHub organisation
Launch
Launch
Administrator
(human)
Admini...
SSH Key + PAT
Passed to Terraform
SSH Key + PAT...
Acts as
Acts as
Key Vault
Access Token
Acc...
Create/Edit/Delete
Create/Edit/Delete
Administrative
GitHub Account
Administ...
SSH Key
SSH...
Environment Repositories
Environment Repositories
Organization settings
Organiza...
Teams / Permissions
Teams / Pe...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/DPL-Platform/diagrams/render-svg/profiles.svg b/DPL-Platform/diagrams/render-svg/profiles.svg new file mode 100644 index 0000000..6f25f0d --- /dev/null +++ b/DPL-Platform/diagrams/render-svg/profiles.svg @@ -0,0 +1,2 @@ + +
Programmer Repo
Programmer Repo
Fork
Fork
Redaktør
Redakt...
Webmaster
Webmast...
Programmer
Programmer
operator
oper...
vi sites.yaml
task site:sync
vi sites....
DPL CMS Core
Repo
DPL CMS Core...
Develop
Develop
Release
Release
Deploy
Deploy
Run
Run
Permissions
+ use update module
+ enable module
Permissions...
Modules
+ update (core)
Modules...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/DPL-Platform/diagrams/render-svg/request-path.svg b/DPL-Platform/diagrams/render-svg/request-path.svg new file mode 100644 index 0000000..f5f5223 --- /dev/null +++ b/DPL-Platform/diagrams/render-svg/request-path.svg @@ -0,0 +1,2 @@ + +
site
site
12%
33k rpd
~1 rps
12%...
Ingress
Nginx
Ingress...
100%:
275k rpd
9.5 rps
100%:...
Internet
Internet
50%
17k rpd
~ 0.5 rps
50%...
Varnish
Varnish
100%
17k rpd
~ 0.5 rps
100%...
Nginx
Nginx
of all searches: 12%
6500 rpd
0,2 rps
of all searches: 12%...
PHP-FPM
PHP-FPM
(replicas)
(replicas)
Redis
Redis
Search
Search
(replicas)
(replicas)
MariaDB
MariaDB
Each link is annotated with
- The percentage of traffic expected from previous link
- Expected requests pr. day
- Requests pr second assuming a 8 hour day
Each link is annotated with...
Estemated Capacities
Estemated Capacities
Ingress
Nginx
Ingress...
Varnish
Varnish
Nginx
Nginx
PHP-FPM
PHP-FPM
Redis
Redis
(replicas)
(replicas)
No relevant limit
No relevant lim...
500 MB (Lagoon default)
500 MB (Lagoon default)
No relevant limit
No relevant lim...
100 MB (Lagoon default)
100 MB (Lagoon default)
~5rps depending  available memory on node (php memory_limit= 400MB)
~5rps depending  available...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/DPL-Platform/diagrams/render-svg/terraform_overview.svg b/DPL-Platform/diagrams/render-svg/terraform_overview.svg new file mode 100644 index 0000000..47dbccd --- /dev/null +++ b/DPL-Platform/diagrams/render-svg/terraform_overview.svg @@ -0,0 +1,54 @@ +SubscriptionResource Group: TerraformStorage accountKey VaultResource Group: MainTerraform stateStorage Account KeyResources...Continuous IntegrationOperatorTerraform1: Unlocks storage account key2: Reads state3: Provisions resources and reconciles stateUsesUses \ No newline at end of file diff --git a/DPL-Platform/diagrams/request-path.drawio b/DPL-Platform/diagrams/request-path.drawio new file mode 100644 index 0000000..543ed20 --- /dev/null +++ b/DPL-Platform/diagrams/request-path.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/DPL-Platform/diagrams/terraform-setup.puml b/DPL-Platform/diagrams/terraform-setup.puml new file mode 100644 index 0000000..6163bff --- /dev/null +++ b/DPL-Platform/diagrams/terraform-setup.puml @@ -0,0 +1,28 @@ +@startuml terraform_overview +actor "Continuous Integration" as ci +actor "Operator" as ops +agent "Terraform" as tf + +node "Subscription" as sub { + package "Resource Group: Terraform" as rgterra { + package "Storage account" { + storage "Terraform state" as tfstate + } + + package "Key Vault" { + storage "Storage Account Key" as storagekey + } + } + package "Resource Group: Main" as rgmain { + card "Resources..." + } +} + +tf <--> storagekey: 1: Unlocks storage account key +tf <--> tfstate: 2: Reads state +tf --> rgmain: 3: Provisions resources and reconciles state + +ops -> tf: Uses +ci -> tf: Uses + +@enduml diff --git a/DPL-Platform/images/lagoon-ui-tasks-page.png b/DPL-Platform/images/lagoon-ui-tasks-page.png new file mode 100644 index 0000000..0f1a9e5 Binary files /dev/null and b/DPL-Platform/images/lagoon-ui-tasks-page.png differ diff --git a/DPL-Platform/images/loki-grafana-download-logs.png b/DPL-Platform/images/loki-grafana-download-logs.png new file mode 100644 index 0000000..6cb47f3 Binary files /dev/null and b/DPL-Platform/images/loki-grafana-download-logs.png differ diff --git a/DPL-Platform/index.html b/DPL-Platform/index.html new file mode 100644 index 0000000..2c15509 --- /dev/null +++ b/DPL-Platform/index.html @@ -0,0 +1,3112 @@ + + + + + + + + + + + + + + + + + + + + + + + DPL Platform Documentation - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL Platform Documentation

+

This directory contains the documentation of the DPL Platforms architecture and +overall concepts.

+

Documentation of how to use the various sub-components of the project can be +found in READMEs in the respective components directory.

+

Table of contents

+ + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/infrastructure/index.html b/DPL-Platform/infrastructure/index.html new file mode 100644 index 0000000..ffdea40 --- /dev/null +++ b/DPL-Platform/infrastructure/index.html @@ -0,0 +1,3369 @@ + + + + + + + + + + + + + + + + + + + + + + + DPL Platform Infrastructure - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

DPL Platform Infrastructure

+

This directory contains the Infrastructure as Code and scripts that are used +for maintaining the infrastructure-component that each platform environment +consists of. A "platform environment" is an umbrella term for the Azure +infrastructure, the Kubernetes cluster, the Lagoon installation and the set of +GitHub environments that makes up a single DPL Platform installation.

+

Directory layout

+
    +
  • dpladm/: a tool used for deploying individual sites. The tools can + be run manually, but the recommended way is via the common infrastructure Taskfile.
  • +
  • environments/: contains a directory for each platform environment.
  • +
  • terraform: terraform setup and tooling that is shared between + environments.
  • +
  • task/: Configuration and scripts used by our Taskfile-based automation + The scripts included in this directory can be run by hand in an emergency + but te recommended way to invoke these via task.
  • +
  • Taskfile.yml: the common infrastructure task + configuration. Invoke task to get a list of targets. Must be run from within + an instance of DPL shell unless otherwise + noted.
  • +
+

Platform Environment configurations

+

The environments directory contains a subdirectory for each platform +environment. You generally interact with the files and directories within the +directory to configure the environment. When a modification has been made, it +is put in to effect by running the appropiate task :

+
    +
  • configuration: contains the various configurations the + applications that are installed on top of the infrastructure requires. These + are used by the support:provision:* tasks.
  • +
  • env_repos contains the Terraform root-module for provisioning GitHub site- + environment repositories. The module is run via the env_repos:provision task.
  • +
  • infrastructure: contains the Terraform root-module used to provision the basic + Azure infrastructure components that the platform requires.The module is run + via the infra:provision task.
  • +
  • lagoon: contains Kubernetes manifests and Helm values-files used for installing + the Lagoon Core and Remote that is at the heart of a DPL Platform installation. + THe module is run via the lagoon:provision:* tasks.
  • +
+

Basic usage of dplsh and an environment configuration

+

The remaining guides in this document assumes that you work from an instance +of the DPL shell. See the +DPLSH Runbook for a basic introduction +to how to use dplsh.

+

Installing a platform environment from scratch

+

The following describes how to set up a whole new platform environment to host + platform sites.

+

The easiest way to set up a new environment is to create a new environments/<name> +directory and copy the contents of an existing environment replacing any +references to the previous environment with a new value corresponding to the new +environment. Take note of the various URLs, and make sure to update the +Current Platform environments +documentation.

+

If this is the very first environment, remember to first initialize the Terraform- +setup, see the terraform README.md.

+

Provisioning infrastructure

+

When you have prepared the environment directory, launch dplsh and go through +the following steps to provision the infrastructure:

+
# We export the variable to simplify the example, you can also specify it inline.
+export DPLPLAT_ENV=dplplat01
+
+# Provision the Azure resources
+task infra:provision
+
+# Create DNS record
+Create an A record in the administration area of your DNS provider.
+Take the terraform output: "ingress_ip" of the former command and create an entry
+like: "*.[DOMAN_NAME].[TLD]": "[ingress_ip]"
+
+# Provision the support software that the Platform relies on
+task support:provision
+
+

Installing and configuring Lagoon

+

The previous step has established the raw infrastructure and the Kubernetes support +projects that Lagoon needs to function. You can proceed to follow the official +Lagoon installation procedure.

+

The execution of the individual steps of the guide has been somewhat automated, +the following describes how to use the automation, make sure to follow along +in the official documentation to understand the steps and some of the +additional actions you have to take.

+
# The following must be carried out from within dplsh, launched as described
+# in the previous step including the definition of DPLPLAT_ENV.
+
+# 1. Provision a lagoon core into the cluster.
+task lagoon:provision:core
+
+# 2. Skip the steps in the documentation that speaks about setting up email, as
+# we currently do not support sending emails.
+
+# 3. Setup ssh-keys for the lagoonadmin user
+# Access the Lagoon UI (consult the platform-environments.md for the url) and
+# log in with lagoonadmin + the admin password that can be extracted from a
+# Kubernetes secret:
+kubectl \
+  -o jsonpath="{.data.KEYCLOAK_LAGOON_ADMIN_PASSWORD}" \
+  -n lagoon-core \
+  get secret lagoon-core-keycloak \
+| base64 --decode
+
+# Then go to settings and add the ssh-keys that should be able to access the
+# lagoon admin user. Consider keeping this list short, and instead add
+# additional users with fewer privileges laster.
+
+# 4. If your ssh-key is passphrase-projected we'll need to setup an ssh-agent
+# instance:
+$ eval $(ssh-agent); ssh-add
+
+# 5. Configure the CLI to verify that access (the cli itself has already been
+#    installed in dplsh)
+task lagoon:cli:config
+
+# You can now add additional users, this step is currently skipped.
+
+# (6. Install Harbor.)
+# This step has already been performed as a part of the installation of
+# support software.
+
+# 7. Install a Lagoon Remote into the cluster
+task lagoon:provision:remote
+
+# 8. Register the cluster administered by the Remote with Lagoon Core
+# Notice that you must provide a bearer token via the USER_TOKEN environment-
+# variable. The token can be found in $HOME/.lagoon.yml after a successful
+# "lagoon login"
+USER_TOKEN=<token> task lagoon:add:cluster:
+
+

The Lagoon core has now been installed, and the remote registered with it.

+

Setting up a GitHub organization and repositories for a new platform environment

+

Prerequisites:

+
    +
  • An properly authenticated azure CLI (az). See the section on initial + Terraform setup for more details on the requirements
  • +
+

First create a new administrative github user and create a new organization +with the user. The administrative user should only be used for administering +the organization via terraform and its credentials kept as safe as possible! The +accounts password can be used as a last resort for gaining access to the account +and will not be stored in Key Vault. Thus, make sure to store the password +somewhere safe, eg. in a password-manager or as a physical printout.

+

This requires the infrastructure to have been created as we're going to store +credentials into the azure Key Vault.

+
# cd into the infrastructure folder and launch a shell
+(host)$ cd infrastructure
+(host)$ dplsh
+
+# Remaining commands are run from within dplsh
+
+# export the platform environment name.
+# export DPLPLAT_ENV=<name>, eg
+$ export DPLPLAT_ENV=dplplat01
+
+# 1. Create a ssh keypair for the user, eg by running
+# ssh-keygen -t ed25519 -C "<comment>" -f dplplatinfra01_id_ed25519
+# eg.
+$ ssh-keygen -t ed25519 -C "dplplatinfra@0120211014073225" -f dplplatinfra01_id_ed25519
+
+# 2. Then access github and add the public-part of the key to the account
+# 3. Add the key to keyvault under the key name "github-infra-admin-ssh-key"
+# eg.
+$ SECRET_KEY=github-infra-admin-ssh-key SECRET_VALUE=$(cat dplplatinfra01_id_ed25519)\
+  task infra:keyvault:secret:set
+
+# 4. Access GitHub again, and generate a Personal Access Token for the account.
+#    The token should
+#     - be named after the platform environment (eg. dplplat01-terraform-timestamp)
+#     - Have a fairly long expiration - do remember to renew it
+#     - Have the following permissions: admin:org, delete_repo, read:packages, repo
+# 5. Add the access token to Key Vault under the name "github-infra-admin-pat"
+# eg.
+$ SECRET_KEY=github-infra-admin-pat SECRET_VALUE=githubtokengoeshere task infra:keyvault:secret:set
+
+# Our tooling can now administer the GitHub organization
+
+

Renewing the administrative GitHub Personal Access Token

+

The Personal Access Token we use for impersonating the administrative GitHub +user needs to be recreated periodically:

+
# cd into the infrastructure folder and launch a shell
+(host)$ cd infrastructure
+(host)$ dplsh
+
+# Remaining commands are run from within dplsh
+
+# export the platform environment name.
+# export DPLPLAT_ENV=<name>, eg
+$ export DPLPLAT_ENV=dplplat01
+
+# 1. Access GitHub, and generate a Personal Access Token for the account.
+#    The token should
+#     - be named after the platform environment (eg. dplplat01-terraform)
+#     - Have a fairly long expiration - do remember to renew it
+#     - Have the following permissions: admin:org, delete_repo, read:packages, repo
+# 2. Add the access token to Key Vault under the name "github-infra-admin-pat"
+# eg.
+$ SECRET_KEY=github-infra-admin-pat SECRET_VALUE=githubtokengoeshere \
+  task infra:keyvault:secret:set
+
+# 3. Delete the previous token
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/infrastructure/terraform/index.html b/DPL-Platform/infrastructure/terraform/index.html new file mode 100644 index 0000000..5e88595 --- /dev/null +++ b/DPL-Platform/infrastructure/terraform/index.html @@ -0,0 +1,3273 @@ + + + + + + + + + + + + + + + + + + + + + + + Terraform - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + + + + +

Terraform

+

This directory contains the configuration and tooling we use to support our +use of terraform.

+

The Terraform setup

+

The setup keeps a single terraform-state pr. environment. Each state is kept as +separate blobs in a Azure Storage Account.

+

Overview of the Terraform setup

+

Access to the storage account is granted via a Storage Account Key which is +kept in a Azure Key Vault in the same resource-group. The key vault, storage account +and the resource-group that contains these resources are the only resources +that are not provisioned via Terraform.

+

Initial setup of Terraform

+

The following procedure must be carried out before the first environment can be +created.

+

Prerequisites:

+
    +
  • A Azure subscription
  • +
  • An authenticated azure CLI that is allowed to use create resources and grant + access to these resources under the subscription including Key Vaults. + The easiest way to achieve this is to grant the user the Owner and + Key Vault Administrator roles to on subscription.
  • +
+

Use the scripts/bootstrap-tf.sh for bootstrapping. After the script has been +run successfully it outputs instructions for how to set up a terraform module +that uses the newly created storage-account for state-tracking.

+

As a final step you must grant any administrative users that are to use the setup +permission to read from the created key vault.

+

Dnsimple

+

The setup uses an integration with DNSimple to set a domain name when the +environments ingress ip has been provisioned. To use this integration first +obtain a api-key for +the DNSimple account. Then use scripts/add-dnsimple-apikey.sh to write it to +the setups Key Vault and finally add the following section to .dplsh.profile ( +get the subscription id and key vault name from existing export for ARM_ACCESS_KEY).

+
export DNSIMPLE_TOKEN=$(az keyvault secret show --subscription "<subscriptionid>"\
+ --name dnsimple-api-key --vault-name <key vault-name> --query value -o tsv)
+export DNSIMPLE_ACCOUNT="<dnsimple-account-id>"
+
+

Terraform Setups

+

A setup is used to manage a set of environments. We currently have a single that +manages all environments.

+

Alpha

+
    +
  • Name: alpha
  • +
  • Resource-group: rg-tfstate-alpha
  • +
  • Key Vault name: kv-dpltfstatealpha001
  • +
  • Storage account: stdpltfstatealpha001
  • +
+

Terraform Modules

+

Root module

+

The platform environments share a number of general modules, which are then +used via a number of root-modules set up for each environment.

+

Consult the general environment documentation +for descriptions on which resources you can expect to find in an environment and +how they are used.

+

Consult the environment overview for an overview of +environments.

+

DPL Platform Infrastructure Module

+

The dpl-platform-environment Terraform module +provisions all resources that are required for a single DPL Platform Environment.

+

Inspect variables.tf for a description +of the required module-variables.

+

Inspect outputs.tf for a list of outputs.

+

Inspect the individual module files for documentation of the resources.

+

The following diagram depicts (amongst other things) the provisioned resources. +Consult the platform environment documentation +for more details on the role the various resources plays. +The Azure infrastructure

+

DPL Platform Site Environment Module

+

The dpl-platform-env-repos Terraform module provisions +the GitHub Git repositories that the platform uses to integrate with Lagoon. Each +site hosted on the platform has a registry.

+

Inspect variables.tf for a description +of the required module-variables.

+

Inspect outputs.tf for a list of outputs.

+

Inspect the individual module files for documentation of the resources.

+

The following diagram depicts how the module gets its credentials for accessing +GitHub and what it provisions. +Provisioning Github infrastructure

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/platform-environments/index.html b/DPL-Platform/platform-environments/index.html new file mode 100644 index 0000000..9ffb51c --- /dev/null +++ b/DPL-Platform/platform-environments/index.html @@ -0,0 +1,3256 @@ + + + + + + + + + + + + + + + + + + + + + + + Current Platform environments - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Current Platform environments

+

dplplat01

+

Roots

+ +

URLs

+ +

Lagoon CLI configuration

+ +

Obtaining Lagoon CLI configuration

+

See Connecting the Lagoon CLI

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/access-kubernetes/index.html b/DPL-Platform/runbooks/access-kubernetes/index.html new file mode 100644 index 0000000..9666d4b --- /dev/null +++ b/DPL-Platform/runbooks/access-kubernetes/index.html @@ -0,0 +1,3207 @@ + + + + + + + + + + + + + + + + + + + + + + + Access Kubernetes - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Access Kubernetes

+

When to use

+

When you need to gain kubectl access to a platform-environments Kubernetes cluster.

+

Prerequisites

+
    +
  • An authenticated az cli (from the host). This likely means az login + --tenant TENANT_ID, where the tenant id is that of "DPL Platform". See Azure + Portal > Tenant Properties. The logged in user must have permissions to list + cluster credentials.
  • +
  • docker cli which is authenticated against the GitHub Container Registry. + The access token used must have the read:packages scope.
  • +
+

Procedure

+
    +
  1. cd to dpl-platform/infrastructure
  2. +
  3. Launch the dpl shell: dplsh
  4. +
  5. Set the platform envionment, eg. for "dplplat01": export DPLPLAT_ENV=dplplat01
  6. +
  7. Authenticate: task cluster:auth
  8. +
+

Your dplsh session should now be authenticated against the cluster.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/add-generic-site-to-platform/index.html b/DPL-Platform/runbooks/add-generic-site-to-platform/index.html new file mode 100644 index 0000000..ad4de7e --- /dev/null +++ b/DPL-Platform/runbooks/add-generic-site-to-platform/index.html @@ -0,0 +1,3250 @@ + + + + + + + + + + + + + + + + + + + + + + + Add a generic site to the platform - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Add a generic site to the platform

+

When to use

+

When you want to add a "generic" site to the platform. By Generic we mean a site +stored in a repository that that is Prepared for Lagoon +and contains a .lagoon.yml at its root.

+

The current main example of such as site is dpl-cms +which is used to develop the shared DPL install profile.

+

Prerequisites

+
    +
  • An authenticated az cli. The logged in user must have full administrative + permissions to the platforms azure infrastructure.
  • +
  • A running dplsh with DPLPLAT_ENV set to the platform + environment name.
  • +
  • A Lagoon account on the Lagoon core with your ssh-key associated (created through + the Lagoon UI, on the Settings page)
  • +
  • The git-url for the sites environment repository (you don't need to create this + repository - it will be created by the task below - but the URL must match the + Github repository that will be created)
  • +
  • A personal access-token that is allowed to pull images from the image-registry + that hosts our images.
  • +
  • The platform environment name (Consult the platform environment documentation)
  • +
+

Procedure

+

The following describes a semi-automated version of "Add a Project" in +the official documentation.

+
# From within dplsh:
+
+# Set an environment,
+# export DPLPLAT_ENV=<platform environment name>
+# eg.
+$ export DPLPLAT_ENV=dplplat01
+
+# If your ssh-key is passphrase-projected we'll need to setup an ssh-agent
+# instance:
+$ eval $(ssh-agent); ssh-add
+
+# 1. Add a project
+# PROJECT_NAME=<project name>  GIT_URL=<url> task lagoon:project:add
+$ PROJECT_NAME=dpl-cms GIT_URL=git@github.com:danskernesdigitalebibliotek/dpl-cms.git\
+  task lagoon:project:add
+
+# 1.b You can also run lagoon add project manually, consult the documentation linked
+#     in the beginning of this section for details.
+
+# 2. Deployment key
+# The project is added, and a deployment key is printed. Copy it and configure
+# the GitHub repository. See the official documentation for examples.
+
+# 3. Webhook
+# Configure Github to post events to Lagoons webhook url.
+# The webhook url for the environment will be
+#  https://webhookhandler.lagoon.<environment>.dpl.reload.dk
+# eg for the environment dplplat01
+#  https://webhookhandler.lagoon.dplplat01.dpl.reload.dk
+#
+# Referer to the official documentation linked above for an example on how to
+# set up webhooks in github.
+
+# 4. Trigger a deployment manually, this will fail as the repository is empty
+#    but will serve to prepare Lagoon for future deployments.
+# lagoon deploy branch -p <project-name> -b <branch>
+$ lagoon deploy branch -p dpl-cms -b main
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/add-library-site-to-platform/index.html b/DPL-Platform/runbooks/add-library-site-to-platform/index.html new file mode 100644 index 0000000..554e5c5 --- /dev/null +++ b/DPL-Platform/runbooks/add-library-site-to-platform/index.html @@ -0,0 +1,3334 @@ + + + + + + + + + + + + + + + + + + + + + + + Add a new library site to the platform - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Add a new library site to the platform

+

When to use

+

When you want to add a new core-test, editor, webmaster or programmer dpl-cms +site to the platform.

+

Prerequisites

+
    +
  • An authenticated az cli. The logged in user must have full administrative + permissions to the platforms azure infrastructure.
  • +
  • A running dplsh with DPLPLAT_ENV set to the platform + environment name.
  • +
+

Procedure

+

The following sections describes how to

+
    +
  • Add the site to sites.yaml
  • +
  • Using the sites.yaml specification to provision Github repositories, + create Lagoon projects, tie the two together, and deploy all the + relevant environments.
  • +
+

Step 1, update sites.yaml

+

Create an entry for the site in sites.yaml.

+

Specify a unique site key (its key in the map of sites), name and description. +Leave out the deployment-key, it will be added automatically by the +synchronization script.

+

Sample entry (beware that this example be out of sync with the environment you +are operating, so make sure to compare it with existing entries from the +environment)

+
sites:
+  bib-rb:
+    name: "Roskilde Bibliotek"
+    description: "Roskilde Bibliotek"
+    primary-domain: "www.roskildebib.dk"
+    secondary-domains: ["roskildebib.dk"]
+    dpl-cms-release: "1.2.3"
+    << : *default-release-image-source
+
+

The last entry merges in a default set of properties for the source of release- +images. If the site is on the "programmer" plan, specify a custom set of +properties like so:

+
sites:
+  bib-rb:
+    name: "Roskilde Bibliotek"
+    description: "Roskilde Bibliotek"
+    primary-domain: "www.roskildebib.dk"
+    secondary-domains: ["roskildebib.dk"]
+    dpl-cms-release: "1.2.3"
+    # Github package registry used as an example here, but any registry will
+    # work.
+    releaseImageRepository: ghcr.io/some-github-org
+    releaseImageName: some-image-name
+
+

Be aware that the referenced images needs to be publicly available as Lagoon +currently only authenticates against ghcr.io.

+

Sites on the "webmaster" plan must have the field plan set to "webmaster" +as this indicates that an environment for testing custom Drupal modules should +also be deployed. For example:

+
sites:
+  bib-rb:
+    name: "Roskilde Bibliotek"
+    description: "Roskilde Bibliotek"
+    primary-domain: "www.roskildebib.dk"
+    secondary-domains: ["roskildebib.dk"]
+    dpl-cms-release: "1.2.3"
+    plan: webmaster
+    << : *default-release-image-source
+
+

The field plan defaults to standard.

+

Now you are ready to sync the site state.

+

Synchronize site state for all sites

+

You have made a single additive change to the sites.yaml file. It is +important to ensure that your branch is otherwise up-to-date with main, +as the state you currently have in sites.yaml is what will be ensured exists +in the platform.

+

You may end up undoing changes that other people have done, if you don't have +their changes to sites.yaml in your branch.

+

Prerequisites:

+
    +
  • A Lagoon account on the Lagoon core with your ssh-key associated (created through + the Lagoon UI, on the Settings page)
  • +
+

From within dplsh run the sites:sync task to sync the site state in +sites.yaml, creating your new site

+
task sites:sync
+
+

You may be prompted to confirm Terraform plan execution and approve other +critical steps. Read and consider these messages carefully and ensure you are +not needlessly changing other sites.

+

The synchronization process:

+
    +
  • ensures a Github repo is provisioned for each site
  • +
  • creates a Lagoon configuration for each site and pushes it to the relevant + branches in the repo (for example, sites with plan: "webmaster" also get + a moduletest branch for testing custom Drupal modules)
  • +
  • ensures a Lagoon project is created for each site
  • +
  • configures Lagoon to track and deploy all the relevant branches for each site + as environments
  • +
+

If no other changes have been made to sites.yaml, the result is that your new +site is created and deployed to all relevant environments.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/changing-and-releasing-new-dplsh-version/index.html b/DPL-Platform/runbooks/changing-and-releasing-new-dplsh-version/index.html new file mode 100644 index 0000000..ef6f69e --- /dev/null +++ b/DPL-Platform/runbooks/changing-and-releasing-new-dplsh-version/index.html @@ -0,0 +1,3189 @@ + + + + + + + + + + + + + + + + + + + + + + + Make changes to DPLSH - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Make changes to DPLSH

+

When to use

+

When for example the kubectl or other dependencies needs updating

+

Make the change

+
    +
  1. Go to the DPLSH directory and make the necessary changes on a new branch
  2. +
  3. Build DPLSH locally by running IMAGE_URL=dplsh IMAGE_TAG=someTagName + task build
  4. +
  5. Test that it works by running DPLSH_IMAGE=dplsh:local ./dplsh and running + what ever commands need to be run to test that the change has the desired effect
  6. +
  7. Check what version DPLSH is at here: + https://github.com/danskernesdigitalebibliotek/dpl-platform/releases
  8. +
  9. Push the branch, have it review and merge it into main
  10. +
  11. Push a new tag to main. The tag should look like this: dplsh-x.x.x. + (If in doubt about what version to bump to; read this: https://semver.org/)
  12. +
  13. Wait for main to automically build and release the new version
  14. +
  15. Go to your main branch, enter the /infrastructure directory and + run ../tools/dplsh/dplsh.sh --update.
  16. +
+

You are done and have the newest version of DPLSH on your machine.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/connecting-the-lagoon-cli/index.html b/DPL-Platform/runbooks/connecting-the-lagoon-cli/index.html new file mode 100644 index 0000000..a0e1d63 --- /dev/null +++ b/DPL-Platform/runbooks/connecting-the-lagoon-cli/index.html @@ -0,0 +1,3316 @@ + + + + + + + + + + + + + + + + + + + + + + + Connecting the Lagoon CLI - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Connecting the Lagoon CLI

+

When to use

+

When you want to use the Lagoon API via the CLI. You can connect from the DPL +Shell, or from a local installation of the CLI.

+

Using the DPL Shell requires administrative privileges to the infrastructure while +a local CLI may connect using only the ssh-key associated to a Lagoon user.

+

This runbook documents both cases, as well as how an administrator can extract the +basic connection details a standard user needs to connect to the Lagoon installation.

+

Prerequisites

+
    +
  • Your ssh-key associated with a lagoon user. This has to be done via the Lagoon + UI by either you for your personal account, or by an administrator who has + access to edit your Lagoon account.
  • +
  • For local installations of the cli:
  • +
  • The Lagoon CLI installed locally
  • +
  • Connectivity details for the Lagoon environment
  • +
  • For administrative access to extract connection details or use the lagoon cli + from within the dpl shell:
  • +
  • A valid dplsh setup to extract the connectivity details
  • +
+

Procedure

+

Obtain the connection details for the environment

+

You can skip this step and go to Configure your local lagoon cli) +if your environment is already in Current Platform environments +and you just want to have a local lagoon cli working.

+

If it is missing, go through the steps below and update the document if you have +access, or ask someone who has.

+
# Launch dplsh.
+$ cd infrastructure
+$ dplsh
+
+# 1. Set an environment,
+# export DPLPLAT_ENV=<platform environment name>
+# eg.
+$ export DPLPLAT_ENV=dplplat01
+
+# 2. Authenticate against AKS, needed by infrastructure and Lagoon tasks
+$ task cluster:auth
+
+# 3. Generate the Lagoon CLI configuration and authenticate
+# The Lagoon CLI is authenticated via ssh-keys. DPLSH will mount your .ssh
+# folder from your homedir, but if your keys are passphrase protected, we need
+# to unlock them.
+$ eval $(ssh-agent); ssh-add
+# Authorize the lagoon cli
+$ task lagoon:cli:config
+
+# List the connection details
+$ lagoon config list
+
+

Configure your local lagoon cli

+

Get the details in the angle-brackets from +Current Platform environments:

+
$ lagoon config add \
+    --graphql https://<GraphQL endpoint> \
+    --ui https://<Lagoon UI> \
+    --hostname <SSH host> \
+    --ssh-key <SSH Key Path> \
+    --port <SSH port>> \
+    --lagoon <Lagoon name>
+
+# Eg.
+$ lagoon config add \
+    --graphql https://api.lagoon.dplplat01.dpl.reload.dk/graphql \
+    --force \
+    --ui https://ui.lagoon.dplplat01.dpl.reload.dk \
+    --hostname 20.238.147.183 \
+    --port 22 \
+    --lagoon dplplat01
+
+

Then log in:

+
# Set the configuration as default.
+lagoon config default --lagoon <Lagoon name>
+lagoon login
+lagoon whoami
+
+# Eg.
+lagoon config default --lagoon dplplat01
+lagoon login
+lagoon whoami
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/index.html b/DPL-Platform/runbooks/index.html new file mode 100644 index 0000000..b880c96 --- /dev/null +++ b/DPL-Platform/runbooks/index.html @@ -0,0 +1,3113 @@ + + + + + + + + + + + + + + + + + + + + + + + DPL Platform Runbooks - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL Platform Runbooks

+

This directory contains our operational runbooks for standard procedures +you may need to carry out while maintaining and operating a DPL Platform +environment.

+

Most runbooks has the following layout.

+
    +
  • Title - Short title that follows the name of the markdown file for quick + lookup.
  • +
  • When to use - Outlines when this runbook should be used.
  • +
  • Prerequisites - Any requirements that should be met before the procedure is + followed.
  • +
  • Procedure - Stepwise description of the procedure, sometimes these will + be whole subheadings, sometimes just a single section with lists.
  • +
+

The runbooks should focus on the "How", and avoid explaining any unnecessary +details.

+ + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/rabbitmq-broker/index.html b/DPL-Platform/runbooks/rabbitmq-broker/index.html new file mode 100644 index 0000000..4ddccee --- /dev/null +++ b/DPL-Platform/runbooks/rabbitmq-broker/index.html @@ -0,0 +1,3216 @@ + + + + + + + + + + + + + + + + + + + + + + + RabbitMQ broker force start - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

RabbitMQ broker force start

+

When to use

+

When the PR environments are no longer being created, and the +lagoon-core-broker-<n> pods are missing or not running, and the container logs +contain errors like Error while waiting for Mnesia tables: +{timeout_waiting_for_tables.

+

This situation is caused by the RabbitMQ broker not starting correctly.

+

Prerequisites

+ +

Procedure

+

You are going to exec into the pod and stop the RabbitMQ application, and then +start it with the force_boot +feature, so that it can +perform its Mnesia sync correctly.

+

Exec into the pod:

+
dplsh:~/host_mount$ kubectl -n lagoon-core exec -ti pod/lagoon-core-broker-0 -- sh
+
+

Stop RabbitMQ:

+
/ $ rabbitmqctl stop_app
+Stopping rabbit application on node rabbit@lagoon-core-broker-0.lagoon-
+core-broker-headless.lagoon-core.svc.cluster.local ...
+
+

Start it immediately after using the force_boot flag:

+
/ $ rabbitmqctl force_boot
+
+

Then exit the shell and check the container logs for one of the broker pods. It +should start without errors.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/remove-site-from-platform/index.html b/DPL-Platform/runbooks/remove-site-from-platform/index.html new file mode 100644 index 0000000..2ff98ca --- /dev/null +++ b/DPL-Platform/runbooks/remove-site-from-platform/index.html @@ -0,0 +1,3342 @@ + + + + + + + + + + + + + + + + + + + + + + + Removing a site from the platform - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Removing a site from the platform

+

When to use

+

When you wish to delete a site and all its data from the platform

+

Prerequisites:

+
    +
  • A lagoon account with your ssh-key associated (created through + the Lagoon UI, on the Settings page)
  • +
  • The site key (its key in sites.yaml)
  • +
  • A properly authenticated azure CLI (az) that has administrative access to + the cluster running the lagoon installation
  • +
+

Procedure

+

The procedure consists of the following steps (numbers does not correspond to +the numbers in the script below).

+
    +
  1. Download and archive relevant backups
  2. +
  3. Remove the project from Lagoon
  4. +
  5. Delete the project namespace from kubernetes.
  6. +
  7. Delete the site from sites.yaml
  8. +
  9. Delete the site's environment repository
  10. +
+

1. Download and archive relevant backups

+

Your first step should be to secure any backups you think might be relevant to +archive. Whether this step is necessary depends on the site. Consult the +Retrieve and Restore backups runbook for the +operational steps.

+

2. Remove the project from Lagoon

+

You are now ready to perform the actual removal of the site.

+
# Launch dplsh.
+$ cd infrastructure
+$ dplsh
+
+# You are assumed to be inside dplsh from now on.
+
+# Set an environment,
+# export DPLPLAT_ENV=<platform environment name>
+# eg.
+$ export DPLPLAT_ENV=dplplat01
+
+# Setup access to ssh-keys so that the lagoon cli can authenticate.
+$ eval $(ssh-agent); ssh-add
+
+# Authenticate against lagoon
+$ task lagoon:cli:config
+
+# Delete the project from Lagoon
+# lagoon delete project --project <site machine-name>
+$ lagoon delete project  --project core-test1
+
+

3. Delete the project namespace from kubernetes

+
# Authenticate against kubernetes
+$ task cluster:auth
+
+# List the namespaces
+# Identify all the project namespace with the syntax <sitename>-<branchname>
+# eg "core-test1-main" for the main branch for the "core-test1" site.
+$ kubectl get ns
+
+# Delete each site namespace
+# kubectl delete ns <namespace>
+# eg.
+$ kubectl delete ns core-test1-main
+
+

4. Delete the site from sites.yaml

+
# 8. Edit sites.yaml, remove the the entry for the site
+$ vi environments/${DPLPLAT_ENV}/sites.yaml
+
+

5. Delete the site's environment repository

+
# Then have Terraform delete the sites repository.
+$ task env_repos:provision
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/retrieve-restore-backup/index.html b/DPL-Platform/runbooks/retrieve-restore-backup/index.html new file mode 100644 index 0000000..3592672 --- /dev/null +++ b/DPL-Platform/runbooks/retrieve-restore-backup/index.html @@ -0,0 +1,3410 @@ + + + + + + + + + + + + + + + + + + + + + + + Retrieving backups - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Retrieving backups

+

When to use

+

When you wish to download an automatic backup made by Lagoon, and optionally +restore it into an existing site.

+

Prerequisites

+
    +
  • Administrative access to the site in the Lagoon UI
  • +
  • (for restore) administrative cluster-access to the site
  • +
+

Procedure

+

Step overview:

+
    +
  1. Download the backup
  2. +
  3. Upload the backup to relevant pods
  4. +
  5. Extract the backup.
  6. +
  7. Cache clearing
  8. +
  9. Cleanup
  10. +
+

While most steps are different for file and database backups, step 1 is close to +identical for the two guides.

+

Be aware that the guide instructs you to copy the backups to /tmp inside the +cli pod. Depending on the resources available on the node /tmp may not have +enough space in which case you may need to modify the cli deployment to add +a temporary volume, or place the backup inside the existing /site/default/files +folder.

+

Step 1, downloading the backup

+

To download the backup access the Lagoon UI and schedule the retrieval of a + backup. To do this,

+
    +
  1. Log in to the environments Lagoon UI (consult the + environment documentation for the url)
  2. +
  3. Access the site's project
  4. +
  5. Access the site's environment ("main" for production)
  6. +
  7. Click on the "Backups" tab
  8. +
  9. Click on the "Retrieve" button for the backups you wish to download and/or + restore. Use to "Source" column to differentiate the types of backups. + "nginx" are backups of the sites files, while "mariadb" are backups of the + sites database.
  10. +
  11. The Buttons changes to "Downloading..." when pressed, wait for them to + change to "Download", then click them again to download the backup
  12. +
+

Step 2a, restore a database

+

To restore the database we must first copy the backup into a running cli-pod +for a site, and then import the database-dump on top of the running site.

+
    +
  1. Copy the uncompressed mariadb sql file you want to restore into the dpl-platform/infrastructure + folder from which we will launch dplsh
  2. +
  3. Launch dplsh from the infrastructure folder (instructions) + and follow the procedure below:
  4. +
+
# 1. Authenticate against the cluster.
+$ task cluster:auth
+
+# 2. List the namespaces to identify the sites namespace
+# The namespace will be on the form <sitename>-<branchname>
+# eg "bib-rb-main" for the "main" branch for the "bib-rb" site.
+$ kubectl get ns
+
+# 3. Export the name of the namespace as SITE_NS
+# eg.
+$ export SITE_NS=bib-rb-main
+
+# 4. Copy the *mariadb.sql file to the CLI pod in the sites namespace
+# eg.
+kubectl cp \
+  -n $SITE_NS  \
+  *mariadb.sql \
+  $(kubectl -n $SITE_NS get pod -l app.kubernetes.io/instance=cli -o jsonpath="{.items[0].metadata.name}"):/tmp/database-backup.sql
+
+# 5. Have drush inside the CLI-pod import the database and clear out the backup
+kubectl exec \
+  -n $SITE_NS \
+  deployment/cli \
+  -- \
+    bash -c " \
+         echo Verifying file \
+      && test -s /tmp/database-backup.sql \
+         || (echo database-backup.sql is missing or empty && exit 1) \
+      && echo Dropping database \
+      && drush sql-drop -y \
+      && echo Importing backup \
+      && drush sqlc < /tmp/database-backup.sql \
+      && echo Clearing cache \
+      && drush cr \
+      && rm /tmp/database-backup.sql
+    "
+
+

Step 2b, restore a sites files

+

To restore backed up files into a site we must first copy the backup into a +running cli-pod for a site, and then rsync the files on top top of the running +site.

+
    +
  1. Copy tar.gz file into the dpl-platform/infrastructure folder from which we + will launch dplsh
  2. +
  3. Launch dplsh from the infrastructure folder (instructions) + and follow the procedure below:
  4. +
+
# 1. Authenticate against the cluster.
+$ task cluster:auth
+
+# 2. List the namespaces to identify the sites namespace
+# The namespace will be on the form <sitename>-<branchname>
+# eg "bib-rb-main" for the "main" branch for the "bib-rb" site.
+$ kubectl get ns
+
+# 3. Export the name of the namespace as SITE_NS
+# eg.
+$ export SITE_NS=bib-rb-main
+
+# 4. Copy the files tar-ball into the CLI pod in the sites namespace
+# eg.
+kubectl cp \
+  -n $SITE_NS  \
+  backup*-nginx-*.tar.gz \
+  $(kubectl -n $SITE_NS get pod -l app.kubernetes.io/instance=cli -o jsonpath="{.items[0].metadata.name}"):/tmp/files-backup.tar.gz
+
+# 5. Replace the current files with the backup.
+# The following
+# - Verifies the backup exists
+# - Removes the existing sites/default/files
+# - Un-tars the backup into its new location
+# - Fixes permissions and clears the cache
+# - Removes the backup archive
+#
+# These steps can also be performed one by one if you want to.
+kubectl exec \
+  -n $SITE_NS \
+  deployment/cli \
+  -- \
+    bash -c " \
+         echo Verifying file \
+      && test -s /tmp/files-backup.tar.gz \
+         || (echo files-backup.tar.gz is missing or empty && exit 1) \
+      && tar ztf /tmp/files-backup.tar.gz data/nginx &> /dev/null \
+         || (echo could not verify the tar.gz file files-backup.tar && exit 1) \
+      && test -d /app/web/sites/default/files \
+         || (echo Could not find destination /app/web/sites/default/files \
+             && exit 1) \
+      && echo Removing existing sites/default/files \
+      && rm -fr /app/web/sites/default/files \
+      && echo Unpacking backup \
+      && mkdir -p /app/web/sites/default/files \
+      && tar --strip 2 --gzip --extract --file /tmp/files-backup.tar.gz \
+             --directory /app/web/sites/default/files data/nginx \
+      && echo Fixing permissions \
+      && chmod -R 777 /app/web/sites/default/files \
+      && echo Clearing cache \
+      && drush cr \
+      && echo Deleting backup archive \
+      && rm /tmp/files-backup.tar.gz
+    "
+
+#  NOTE: In some situations some files in /app/web/sites/default/files might
+#  be locked by running processes. In that situations delete all the files you
+#  can from /app/web/sites/default/files manually, and then repeat the step
+#  above skipping the removal of /app/web/sites/default/files
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/retrieve-sites-logs/index.html b/DPL-Platform/runbooks/retrieve-sites-logs/index.html new file mode 100644 index 0000000..5e7605c --- /dev/null +++ b/DPL-Platform/runbooks/retrieve-sites-logs/index.html @@ -0,0 +1,3220 @@ + + + + + + + + + + + + + + + + + + + + + + + Retrieve site logs - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Retrieve site logs

+

When to use

+

When you want to inspects the logs produced by as specific site

+

Prerequisites

+
    +
  • Login credentials for Grafana. As a fallback the password for the admin can + password can be fetched from the cluster if it has not been changed via
  • +
+
kubectl get secret \
+  --namespace grafana \
+  -o jsonpath="{.data.admin-password}" \
+  grafana \
+| base64 -d
+
+

Consult the access-kubernetes Run book for instructions +on how to access the cluster.

+

Procedure - Loki / Grafana

+

Using the inspector to download logs

+
    +
  1. Access the environments Grafana installation - consult the + platform-environments.md for the url.
  2. +
  3. Select "Explorer" in the left-most menu and select the "Loki" data source in + the top.
  4. +
  5. Query the logs for the environment by either
  6. +
  7. Use the Log Browser to pick the namespace for the site. It will follow the + pattern <sitename>-<branchname>.
  8. +
  9. Do a custom LogQL, eg. to + fetch all logs from the nginx container for the site "main" branch of the + "rdb" site do query on the form + {app="nginx-php-persistent",container="nginx",namespace="rdb-main"}
  10. +
  11. Eg, for the main branch for the site "rdb": {namespace="rdb-main"}
  12. +
  13. Click "Inspector" -> "Data" -> "Download Logs" to download the log lines.
  14. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/run-a-lagoon-task/index.html b/DPL-Platform/runbooks/run-a-lagoon-task/index.html new file mode 100644 index 0000000..4365525 --- /dev/null +++ b/DPL-Platform/runbooks/run-a-lagoon-task/index.html @@ -0,0 +1,3199 @@ + + + + + + + + + + + + + + + + + + + + + + + Run a Lagoon Task - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Run a Lagoon Task

+

When to use

+

When you need to run a Lagoon task

+

Prerequisites

+

You need access to the Lagoon UI

+

Procedure

+
    +
  • Login to the Lagoon UI.
  • +
  • Navigate to the project you want to access.
  • +
  • Choose the environment you want to interact with.
  • +
  • Click the "Tasks" tab.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/scale-aks/index.html b/DPL-Platform/runbooks/scale-aks/index.html new file mode 100644 index 0000000..5fe32d6 --- /dev/null +++ b/DPL-Platform/runbooks/scale-aks/index.html @@ -0,0 +1,3262 @@ + + + + + + + + + + + + + + + + + + + + + + + Scaling AKS - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Scaling AKS

+

When to use

+

When the cluster is over or underprovisioned and needs to be scaled.

+

Prerequisites

+
    +
  • A running dplsh launched from ./infrastructure with + DPLPLAT_ENV set to the platform environment name.
  • +
+

References

+ +

Procedure

+

There are multiple approaches to scaling AKS. We run with the auto-scaler enabled +which means that in most cases the thing you want to do is to adjust the max or +minimum configuration for the autoscaler.

+ +

Adjusting the autoscaler

+

Edit the infrastructure configuration for your environment. Eg for dplplat01 +edit infrastructure/environments/dplplat01/infrastructure/main.tf.

+

Adjust the ..._count_min / ..._count_min corresponding to the node-pool you +want to grow/shrink.

+

Then run infra:provision to have terraform effect the change.

+
task infra:provision
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/set-environment-variable/index.html b/DPL-Platform/runbooks/set-environment-variable/index.html new file mode 100644 index 0000000..265b852 --- /dev/null +++ b/DPL-Platform/runbooks/set-environment-variable/index.html @@ -0,0 +1,3233 @@ + + + + + + + + + + + + + + + + + + + + + + + Set an environment variable for a site - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Set an environment variable for a site

+

When to use

+

When you wish to set an environment variable on a site. The variable can be +available for either all sites in the project, or for a specific site in the +project.

+

The variables are safe for holding secrets, and as such can be used both for +"normal" configuration values, and secrets such as api-keys.

+

The variabel will be available to all containers in the environment can can be +picked up and parsed eg. in Drupals settings.php.

+

Prerequisites

+
    +
  • A running dplsh with DPLPLAT_ENV set to the platform + environment name and ssh-agent running if your ssh-keys are passphrase + protected.
  • +
  • A Lagoon account on the Lagoon core with your ssh-key associated (created + through the Lagoon UI, on the Settings page)
  • +
+

Procedure

+
# From within a dplsh session authorized to use your ssh keys:
+
+# 1. Authenticate against the cluster and lagoon
+$ task cluster:auth
+$ task lagoon:cli:config
+
+# 2. Refresh your Lagoon token.
+$ lagoon login
+
+# 3a. For project-level variables, use the following command, which creates
+#     the variable if it does not yet exist, or updates it otherwise:
+$ task lagoon:ensure:environment-variable \
+  PROJECT_NAME=<project name> \
+  VARIABLE_SCOPE=RUNTIME \
+  VARIABLE_NAME=<your variable name> \
+  VARIABLE_VALUE=<your variable value>
+
+# 3b. Or, to similarly ensure the value of an environment-level variable:
+$ task lagoon:ensure:environment-variable \
+  PROJECT_NAME=<project name> \
+  ENVIRONMENT_NAME=<environment name> \
+  VARIABLE_SCOPE=RUNTIME \
+  VARIABLE_NAME=<your variable name> \
+  VARIABLE_VALUE=<your variable value>
+
+# If you get a "Invalid Auth Token" your token has probably expired, generated a
+# new with "lagoon login" and try again.
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/ui-sync-site-state/index.html b/DPL-Platform/runbooks/ui-sync-site-state/index.html new file mode 100644 index 0000000..ce12328 --- /dev/null +++ b/DPL-Platform/runbooks/ui-sync-site-state/index.html @@ -0,0 +1,3260 @@ + + + + + + + + + + + + + + + + + + + + + + + UI: Synchronize site state - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

UI: Synchronize site state

+

When to use

+

If you want to synchronize state from one environment into another environment.

+

For example, you may want to synchronize state to a PR environment to run your +code in a more realistic setup.

+

Or you may want to synchronize a main (production) environment state to a +moduletest environment, if requested by the customer.

+

Prerequisites

+
    +
  • A user with access to the relevant project through the + Lagoon UI
  • +
+

If you have access to the dpl-platform setup and can run task in the taskfile +(for platform engineers, not developers of the CMS) you may want to synchronize +site state using the related task (runbook WIP).

+

Procedure

+
    +
  1. Go to the [Lagoon UI] website and log in
  2. +
  3. Navigate to the relevant project by selecting in the list
  4. +
  5. Pick the target environment in the list of environments. E.g. if you are + synchronizing state from main to pr-775 you should select pr-775.
  6. +
  7. In the left-hand side pick the "Tasks" menu point
  8. +
+

Now you are at the tasks UI and can execute tasks for this environment. It +should look something like this:

+

Tasks page in the Lagoon UI

+

Now we need to execute 3 tasks to synchronize the whole state and make it +available on visits to the target site:

+ +
    +
  1. +

    Run task "Copy database between environments [drush sql-sync]":

    +
      +
    • Select the task in the "Select a task..." dropdown.
    • +
    • Select the source environment. E.g. if you are synchronizing from main to + pr-775 select main in the dropdown.
    • +
    • Click "Run task" to start the task.
      + The task appears in the top of the list of tasks. You can click it to see + log output. Once the task completes verify that the log output states that + the synchronization worked.
    • +
    +
  2. +
+ +
    +
  1. +

    Run task "Copy files between environments [drush rsync]":

    +
      +
    • Select the task in the "Select a task..." dropdown.
    • +
    • Select the source environment as above.
    • +
    • Click "Run task" to start the task.
      + The task output can be viewed as described in point 5.
      + The task will fail. Verify that the error is a list of statements saying + > rsync: [receiver] failed to set times on .... As long as these are the + only errors in the output, the synchronization succeeded.
    • +
    +
  2. +
+ +
    +
  1. +

    Run task "Clear Drupal caches [drupal cache-clear]" to clear the caches:

    +
      +
    • Select the task in the "Select a task..." dropdown.
    • +
    • Select the source environment as above.
    • +
    • Click "Run task" to start the task.
      + Once the task completes the environment has been fully synced and caches + are cleared so the state will be reflected when you visit the site.
      + E.g. if you were synchronizing state from main to pr-775, the pr-775 + environment will now have the same state as main.
    • +
    +
  2. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/update-upgrade-status/index.html b/DPL-Platform/runbooks/update-upgrade-status/index.html new file mode 100644 index 0000000..d4f533d --- /dev/null +++ b/DPL-Platform/runbooks/update-upgrade-status/index.html @@ -0,0 +1,3204 @@ + + + + + + + + + + + + + + + + + + + + + + + Update the support workload upgrade status - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Update the support workload upgrade status

+

When to use

+

When you need to update the support workload version sheet.

+

Prerequisites

+ +

Procedure

+

Run dplsh to extract the current and latest version for all support workloads

+
# First authenticate against the cluster
+task cluster:auth
+# Then pull the status
+task ops:get-versions
+
+

Then access the version status sheet +and update the status from the output.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/upgrading-aks/index.html b/DPL-Platform/runbooks/upgrading-aks/index.html new file mode 100644 index 0000000..42ff686 --- /dev/null +++ b/DPL-Platform/runbooks/upgrading-aks/index.html @@ -0,0 +1,3348 @@ + + + + + + + + + + + + + + + + + + + + + + + Upgrading AKS - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Upgrading AKS

+

When to use

+

When you want to upgrade Azure Kubernetes Service to a newer version.

+

Prerequisites

+
    +
  • A running dplsh launched from ./infrastructure with + DPLPLAT_ENV set to the platform environment name.
  • +
  • Knowledge about the version of AKS you wish to upgrade to.
  • +
  • Consult AKS Kubernetes Release Calendar + for a list of the various versions and when they are End of Life
  • +
+

References

+ +

Procedure

+

We use Terraform to upgrade AKS. Should you need to do a manual upgrade consult +Azures documentation on upgrading a cluster +and on upgrading node pools. +Be aware in both cases that the Terraform state needs to be brought into sync +via some means, so this is not a recommended approach.

+

Find out which versions of kubernetes an environment can upgrade to

+

In order to find out which versions of kubernetes we can upgrade to, we need to +use the following command:

+
task cluster:get-upgrades
+
+

This will output a table of in which the column "Upgrades" lists the available +upgrades for the highest available minor versions.

+

A Kubernetes cluster can can at most be upgraded to the nearest minor version, +which means you may be in a situation where you have several versions between +you and the intended version.

+

Minor versions can be skipped, and AKS will accept a cluster being upgraded to +a version that does not specify a patch version. So if you for instance want +to go from 1.20.9 to 1.22.15, you can do 1.21, and then 1.22.15. When +upgrading to 1.21 Azure will substitute the version for an the hightest available +patch version, e.g. 1.21.14.

+

You should know know which version(s) you need to upgrade to, and can continue to +the actual upgrade.

+

Ensuring the Terraform state is in sync

+

As we will be using Terraform to perform the upgrade we want to make sure it its +state is in sync. Execute the following task and resolve any drift:

+
task infra:provision
+
+

Upgrade the cluster

+

Initiate a cluster upgrade. This will upgrade the control plane and node pools +together. See the AKS documentation +for background info on this operation.

+
    +
  1. +

    Update the control_plane_version reference in infrastructure/environments/<environment>/infrastructure/main.tf + and run task infra:provision to apply. You can skip patch-versions, but you + can only do one minor-version at the time

    +
  2. +
  3. +

    Monitor the upgrade as it progresses. The control-plane upgrade is usually + performed in under 5 minutes. Monitor via eg. watch -n 5 kubectl version.

    +
  4. +
  5. +

    AKS will then automatically upgrade the system, admin and application + node-pools.

    +
  6. +
  7. +

    Monitor the upgrade as it progresses. Expect the provisioning of and workload + scheduling to a single node to take about 5-10 minutes. In particular be + aware that the admin node-pool where harbor runs has a tendency to take a + long time as the harbor pvcs are slow to migrate to the new node.

    +

    Monitor via eg.

    +
    watch -n 5 kubectl get nodes
    +
    +
  8. +
  9. +

    Go to dplsh's Dockerfile and update the KUBECTL_VERSION version to + match that of the upgraded AKS version

    +
  10. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/upgrading-lagoon/index.html b/DPL-Platform/runbooks/upgrading-lagoon/index.html new file mode 100644 index 0000000..2e7f9ef --- /dev/null +++ b/DPL-Platform/runbooks/upgrading-lagoon/index.html @@ -0,0 +1,3266 @@ + + + + + + + + + + + + + + + + + + + + + + + Upgrading Lagoon - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Upgrading Lagoon

+

When to use

+

When there is a need to upgrade Lagoon to a new patch or minor version.

+

References

+ +

Prerequisites

+
    +
  • A running dplsh launched from ./infrastructure with + DPLPLAT_ENV set to the platform environment name.
  • +
  • Knowledge about the version of Lagoon you want to upgrade to.
  • +
  • You can extract version (= chart version) and appVersion (= lagoon release + version) for the lagoon-remote / lagoon-core charts via the following commands + (replace lagoon-core for lagoon-remote if necessary).
  • +
+

Lagoon-core:

+
curl -s https://uselagoon.github.io/lagoon-charts/index.yaml \
+  | yq '.entries.lagoon-core[] | [.name, .appVersion, .version, .created] | @tsv'
+
+

Lagoon-remote:

+
curl -s https://uselagoon.github.io/lagoon-charts/index.yaml \
+  | yq '.entries.lagoon-remote[] | [.name, .appVersion, .version, .created] | @tsv'
+
+
    +
  • Knowledge of any breaking changes or necessary actions that may affect the + platform when upgrading. See chart release notes for all intermediate chart + releases.
  • +
+

Procedure

+
    +
  1. Upgrade Lagoon core
      +
    1. Backup the API and Keycloak dbs as described in the official documentation
    2. +
    3. Bump the chart version VERSION_LAGOON_CORE in + infrastructure/environments/<env>/lagoon/lagoon-versions.env
    4. +
    5. Perform a helm diff
        +
      • DIFF=1 task lagoon:provision:core
      • +
      +
    6. +
    7. Perform the actual upgrade
        +
      • task lagoon:provision:core
      • +
      +
    8. +
    +
  2. +
  3. Upgrade Lagoon remote
      +
    1. Bump the chart version VERSION_LAGOON_REMOTE in + infrastructure/environments/<env>/lagoon/lagoon-versions.env
    2. +
    3. Perform a helm diff
        +
      • DIFF=1 task lagoon:provision:remote
      • +
      +
    4. +
    5. Perform the actual upgrade
        +
      • task lagoon:provision:remote
      • +
      +
    6. +
    7. Take note in the output from Helm of any CRD updates that may be required
    8. +
    +
  4. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/upgrading-support-workloads/index.html b/DPL-Platform/runbooks/upgrading-support-workloads/index.html new file mode 100644 index 0000000..7c581b8 --- /dev/null +++ b/DPL-Platform/runbooks/upgrading-support-workloads/index.html @@ -0,0 +1,4201 @@ + + + + + + + + + + + + + + + + + + + + + + + Upgrading Support Workloads - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Upgrading Support Workloads

+

When to use

+

When you want to upgrade support workloads in the cluster. This includes.

+
    +
  • Cert-manager
  • +
  • Grafana
  • +
  • Harbor
  • +
  • Ingress Nginx
  • +
  • K8up
  • +
  • Loki
  • +
  • Minio
  • +
  • Prometheus
  • +
  • Promtail
  • +
+

This document contains general instructions for how to upgrade support workloads, +followed by specific instructions for each workload (linked above).

+

Prerequisites

+ +

General Procedure

+
    +
  1. +

    Identify the version you want to bump in the environment/configuration directory + eg. for dplplat01 infrastructure/environments/dplplat01/configuration/versions.env. + The file contains links to the relevant Artifact Hub pages for the individual + projects and can often be used to determine both the latest version, but also + details about the chart such as how a specific manifest is used. + You can find the latest version of the support workload in the + Version status + sheet which itself is updated via the procedure described in the + Update Upgrade status runbook.

    +
  2. +
  3. +

    Consult any relevant changelog to determine if the upgrade will require any + extra work beside the upgrade itself. + To determine which version to look up in the changelog, be aware of the difference + between the chart version + and the app version. + We currently track the chart versions, and not the actual version of the + application inside the chart. In order to determine the change in appVersion + between chart releases you can do a diff between releases, and keep track of the + appVersion property in the charts Chart.yaml. Using using grafana as an example: + https://github.com/grafana/helm-charts/compare/grafana-6.55.1...grafana-6.56.0. + The exact way to do this differs from chart to chart, and is documented in the + Specific producedures and tests below.

    +
  4. +
  5. +

    Carry out any chart-specific preparations described in the charts update-procedure. + This could be upgrading a Custom Resource Definition that the chart does not + upgrade.

    +
  6. +
  7. +

    Identify the relevant task in the main Taskfile + for upgrading the workload. For example, for cert-manager, the task is called + support:provision:cert-manager and run the task with DIFF=1, eg + DIFF=1 task support:provision:cert-manager.

    +
  8. +
  9. +

    If the diff looks good, run the task without DIFF=1, eg task support:provision:cert-manager.

    +
  10. +
  11. +

    Then proceeded to perform the verification test for the relevant workload. See + the following section for known verification tests.

    +
  12. +
  13. +

    Finally, it is important to verify that Lagoon deployments still work. Some + breaking changes will not manifest themselves until an environment is + rebuilt, at which point it may subsequently fail. An example is the + disabling of user snippets in the ingress-nginx controller + v1.9.0. To verify + deployments still work, log in to the Lagoon UI and select an environment to + redeploy.

    +
  14. +
+

Specific producedures and tests

+ +

Cert Manager

+

Comparing cert-manager versions

+

The project project versions its Helm chart together with the app itself. So, +simply use the chart version in the following checks.

+

Cert Manager keeps Release notes +for the individual minor releases of the project. Consult these for every +upgrade past a minor version.

+

As both are versioned in the same repository, simply use the following link +for looking up the release notes for a specific patch release, replacing the +example tag with the version you wish to upgrade to.

+

https://github.com/cert-manager/cert-manager/releases/tag/v1.11.2

+

To compare two reversions, do the same using the following link:

+

https://github.com/cert-manager/cert-manager/compare/v1.11.1...v1.11.2

+

Upgrade cert-manager

+

Commands

+
# Diff
+DIFF=1 task support:provision:cert-manager
+
+# Upgrade
+task support:provision:cert-manager
+
+

Verify cert-manager upgrade

+

Verify that cert-manager itself and webhook pods are all running and healthy.

+
task support:verify:cert-manager
+
+

Grafana

+

Comparing Grafana versions

+

Insert the chart version in the following link to see the release note.

+

https://github.com/grafana/helm-charts/releases/tag/grafana-6.52.9

+

The note will most likely be empty. Now diff the chart version with the current +version, again replacing the version with the relevant for your releases.

+

https://github.com/grafana/helm-charts/compare/grafana-6.43.3...grafana-6.52.9

+

As the repository contains a lot of charts, you will need to do a bit of +digging. Look for at least charts/grafana/Chart.yaml which can tell you the +app version.

+

With the app-version in hand, you can now look at the release notes for the +grafana app +itself.

+

Upgrade grafana

+

Diff command

+
DIFF=1 task support:provision:grafana
+
+

Upgrade command

+
task support:provision:grafana
+
+

Verify grafana upgrade

+

Verify that the Grafana pods are all running and healthy.

+
kubectl get pods --namespace grafana
+
+

Access the Grafana UI and see if you can log in. If you do not have a user +but have access to read secrets in the grafana namespace, you can retrive the +admin password with the following command:

+
# Password for admin
+UI_NAME=grafana task ui-password
+
+# Hostname for grafana
+kubectl -n grafana get -o jsonpath="{.spec.rules[0].host}" ingress grafana ; echo
+
+

Harbor

+

Comparing Harbor versions

+

Harbor has different app and chart versions.

+

An overview of the chart versions can be retrived +from Github. +the chart does not have a changelog.

+

Link for comparing two chart releases: +https://github.com/goharbor/harbor-helm/compare/v1.10.1...v1.12.0

+

Having identified the relevant appVersions, consult the list of +Harbor releases to see a description +of the changes included in the release in question. If this approach fails you +can also use the diff-command described below to determine which image-tags are +going to change and thus determine the version delta.

+

Harbor is a quite active project, so it may make sense mostly to pay attention +to minor/major releases and ignore the changes made in patch-releases.

+

Upgrade Harbor

+

Harbor documents the general upgrade procedure for non-kubernetes upgrades for minor +versions on their website. +This documentation is of little use to our Kubernetes setup, but it can be useful +to consult the page for minor/major version upgrades to see if there are any +special considerations to be made.

+

The Harbor chart repository has upgrade instructions as well. +The instructions asks you to do a snapshot of the database and backup the tls +secret. Snapshotting the database is currently out of scope, but could be a thing +that is considered in the future. The tls secret is handled by cert-manager, and +as such does not need to be backed up.

+

With knowledge of the app version, you can now update versions.env as described +in the General Procedure section, diff to see the changes +that are going to be applied, and finally do the actual upgrade.

+

Diff command

+
DIFF=1 task support:provision:harbor
+
+

Upgrade command

+
task support:provision:harbor
+
+

Verify Harbor upgrade

+

First verify that pods are coming up

+
kubectl -n harbor get pods
+
+

When Harbor seems to be working, you can verify that the UI is working by +accessing https://harbor.lagoon.dplplat01.dpl.reload.dk/. The password for +the user admin can be retrived with the following command:

+
UI_NAME=harbor task ui-password
+
+

If everything looks good, you can consider to deploying a site. One way to do this +is to identify an existing site of low importance, and re-deploy it. A re-deploy +will require Lagoon to both fetch and push images. Instructions for how to access +the lagoon UI is out of scope of this document, but can be found in the runbook +for running a lagoon task. In this case you are looking +for the "Deploy" button on the sites "Deployments" tab.

+

Ingress-nginx

+

Comparing ingress-nginx versions

+

When working with the ingress-nginx chart we have at least 3 versions to keep +track off.

+

The chart version tracks the version of the chart itself. The charts appVersion +tracks a controller application which dynamically configures a bundles nginx. +The version of nginx used is determined configuration-files in the controller. +Amongst others the +ingress-nginx.yaml.

+

Link for diffing two chart versions: +https://github.com/kubernetes/ingress-nginx/compare/helm-chart-4.6.0...helm-chart-4.6.1

+

The project keeps a quite good changelog for the chart

+

Link for diffing two controller versions: +https://github.com/kubernetes/ingress-nginx/compare/controller-v1.7.1...controller-v1.7.0

+

Consult the individual GitHub releases +for descriptions of what has changed in the controller for a given release.

+

Upgrade ingress-nginx

+

With knowledge of the app version, you can now update versions.env as described +in the General Procedure section, diff to see the changes +that are going to be applied, and finally do the actual upgrade.

+

Diff command

+
DIFF=1 task support:provision:ingress-nginx
+
+

Upgrade command

+
task support:provision:ingress-nginx
+
+

Verify ingress-nginx upgrade

+

The ingress-controller is very central to the operation of all public accessible +parts of the platform. It's area of resposibillity is on the other hand quite +narrow, so it is easy to verify that it is working as expected.

+

First verify that pods are coming up

+
kubectl -n ingress-nginx get pods
+
+

Then verify that the ingress-controller is able to serve traffic. This can be +done by accessing the UI of one of the apps that are deployed in the platform.

+

Access eg. https://ui.lagoon.dplplat01.dpl.reload.dk/.

+

K8up

+

We can currently not upgrade to version 2.x of K8up as Lagoon +is not yet ready

+

Loki

+

Comparing Loki versions

+

The Loki chart is versioned separatly from Loki. The version of Loki installed +by the chart is tracked by its appVersion. So when upgrading, you should always +look at the diff between both the chart and app version.

+

The general upgrade procedure will give you the chart version, access the +following link to get the release note for the chart. Remember to insert your +version:

+

https://github.com/grafana/loki/releases/tag/helm-loki-5.5.1

+

Notice that the Loki helm-chart is maintained in the same repository as Loki +itself. You can find the diff between the chart versions by comparing two +chart release tags.

+

https://github.com/grafana/loki/compare/helm-loki-5.5.0...helm-loki-5.5.1

+

As the repository contains changes to Loki itself as well, you should seek out +the file production/helm/loki/Chart.yaml which contains the appVersion that +defines which version of Loki a given chart release installes.

+

Direct link to the file for a specific tag: +https://github.com/grafana/loki/blob/helm-loki-3.3.1/production/helm/loki/Chart.yaml

+

With the app-version in hand, you can now look at the +release notes for Loki +to see what has changed between the two appVersions.

+

Last but not least the Loki project maintains a upgrading guide that can be +found here: https://grafana.com/docs/loki/latest/upgrading/

+

Upgrade Loki

+

Diff command

+
DIFF=1 task support:provision:loki
+
+

Upgrade command

+
task support:provision:loki
+
+

Verify Loki upgrade

+

List pods in the loki namespace to see if the upgrade has completed +successfully.

+
  kubectl --namespace loki get pods
+
+

Next verify that Loki is still accessibel from Grafana and collects logs by +logging in to Grafana. Then verify the Loki datasource, and search out some +logs for a site. See the validation steps for Grafana +for instructions on how to access the Grafana UI.

+

MinIO

+

We can currently not upgrade MinIO without loosing the Azure blob gateway. +see:

+ +

Prometheus

+

Comparing Prometheus versions

+

The kube-prometheus-stack helm chart is quite well maintained and is versioned +and developed separately from the application itself.

+

A specific release of the chart can be accessed via the following link:

+

https://github.com/prometheus-community/helm-charts/releases/tag/kube-prometheus-stack-45.27.2

+

The chart is developed alongside a number of other community driven prometheus- +related charts in https://github.com/prometheus-community/helm-charts.

+

This means that the following comparison between two releases of the chart +will also contain changes to a number of other charts. You will have to look +for changes in the charts/kube-prometheus-stack/ directory.

+

https://github.com/prometheus-community/helm-charts/compare/kube-prometheus-stack-45.26.0...kube-prometheus-stack-45.27.2

+

Upgrade Prometheus

+

The Readme for the chart contains a good Upgrading Chart +section that describes things to be aware of when upgrading between specific +minor and major versions. The same documentation can also be found on +artifact hub.

+

Consult the section that matches the version you are upgrading from and to. Be +aware that upgrades past a minor version often requires a CRD update. The +CRDs may have to be applied before you can do the diff and upgrade. Once the +CRDs has been applied you are committed to the upgrade as there is no simple +way to downgrade the CRDs.

+

Diff command

+
DIFF=1 task support:provision:prometheus
+
+

Upgrade command

+
task support:provision:prometheus
+
+

Verify Prometheus upgrade

+

List pods in the prometheus namespace to see if the upgrade has completed +successfully. You should expect to see two types of workloads. First a single +a single promstack-kube-prometheus-operator pod that runs Prometheus, and then +a promstack-prometheus-node-exporter pod for each node in the cluster.

+
  kubectl --namespace prometheus get pods -l "release=promstack"
+
+

As the Prometheus UI is not directly exposed, the easiest way to verify that +Prometheus is running is to access the Grafana UI and verify that the dashboards +that uses Prometheus are working, or as a minimum that the prometheus datasource +passes validation. See the validation steps for Grafana +for instructions on how to access the Grafana UI.

+

Promtail

+

Comparing Promtail versions

+

The Promtail chart is versioned separatly from Promtail which itself is a part of +Loki. The version of Promtail installed by the chart is tracked by its appVersion. +So when upgrading, you should always look at the diff between both the chart and +app version.

+

The general upgrade procedure will give you the chart version, access the +following link to get the release note for the chart. Remember to insert your +version:

+

https://github.com/grafana/helm-charts/releases/tag/promtail-6.6.0

+

The note will most likely be empty. Now diff the chart version with the current +version, again replacing the version with the relevant for your releases.

+

https://github.com/grafana/helm-charts/compare/promtail-6.6.0...promtail-6.6.1

+

As the repository contains a lot of charts, you will need to do a bit of +digging. Look for at least charts/promtail/Chart.yaml which can tell you the +app version.

+

With the app-version in hand, you can now look at the +release notes for Loki +(which promtail is part of). Look for notes in the Promtail sections of the +release notes.

+

Upgrade Promtail

+

Diff command

+
DIFF=1 task support:provision:promtail
+
+

Upgrade command

+
task support:provision:promtail
+
+

Verify Promtail upgrade

+

List pods in the promtail namespace to see if the upgrade has completed +successfully.

+
  kubectl --namespace promtail get pods
+
+

With the pods running, you can verify that the logs are being collected seeking +out logs via Grafana. See the validation steps for Grafana +for details on how to access the Grafana UI.

+

You can also inspect the logs of the individual pods via

+
kubectl --namespace promtail logs -l "app.kubernetes.io/name=promtail"
+
+

And verify that there are no obvious error messages.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-Platform/runbooks/using-dplsh/index.html b/DPL-Platform/runbooks/using-dplsh/index.html new file mode 100644 index 0000000..21b4581 --- /dev/null +++ b/DPL-Platform/runbooks/using-dplsh/index.html @@ -0,0 +1,3229 @@ + + + + + + + + + + + + + + + + + + + + + + + Using the DPL Shell - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Using the DPL Shell

+

The Danish Public Libraries Shell (dplsh) is a container-based shell used by +platform-operators for all cli operations.

+

When to use

+

Whenever you perform any administrative actions on the platform. If you want +to know more about the shell itself? Refer to tools/dplsh.

+

Prerequisites

+
    +
  • Docker
  • +
  • jq
  • +
  • Bash 4 or newer
  • +
  • An authorized Azure az cli. The version should match the version found in + FROM mcr.microsoft.com/azure-cli:version in the dplsh Dockerfile + You can choose to authorize the az cli from within dplsh, but your session + will only last as long as the shell-session. The use you authorize as must + have permission to read the Terraform state from the Terraform setup + , and Contributor permissions on the environments + resource-group in order to provision infrastructure.
  • +
  • dplsh.sh symlinked into your path as dplsh, see Launching the Shell + (optional, but assumed below)
  • +
+

Procedure

+
# Launch dplsh.
+$ cd infrastructure
+$ dplsh
+
+# 1. Set an environment,
+# export DPLPLAT_ENV=<platform environment name>
+# eg.
+$ export DPLPLAT_ENV=dplplat01
+
+# 2a. Authenticate against AKS, needed by infrastructure and Lagoon tasks
+$ task cluster:auth
+
+# 2b - if you want to use the Lagoon CLI)
+# The Lagoon CLI is authenticated via ssh-keys. DPLSH will mount your .ssh
+# folder from your homedir, but if your keys are passphrase protected, we need
+# to unlock them.
+$ eval $(ssh-agent); ssh-add
+# Then authorize the lagoon cli
+$ task lagoon:cli:config
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/architecture/adr-001-rehydration/index.html b/DPL-React/architecture/adr-001-rehydration/index.html new file mode 100644 index 0000000..58e21aa --- /dev/null +++ b/DPL-React/architecture/adr-001-rehydration/index.html @@ -0,0 +1,3387 @@ + + + + + + + + + + + + + + + + + + + + + + + Rehydration - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Rehydration

+

Context

+

We are not able to persist and execute a users intentions across page loads. +This is expressed through a number of issues. The main agitator is maintaining +intent whenever a user tries to do anything that requires them to be +authenticated. In these situations they get redirected off the page and after a +successful login they get redirected back to the origin page but without the +intended action fulfilled.

+

One example is the AddToChecklist functionality. Whenever a user wants to add +a material to their checklist they click the "Tilføj til huskelist" button next +to the material presentation. +They then get redirected to Adgangsplatformen. +After a successful login they get redirected back to the material page but the +material has not been added to their checklist.

+

Decision

+

After an intent has been stated we want the intention to be executed even though +a page reload comes in the way.

+

We move to implementing what we define as an explicit intention before the +actual action is tried for executing.

+
    +
  1. User clicks the button.
  2. +
  3. Intent state is generated and committed.
  4. +
  5. Implementation checks if the intended action meets all the requirements. In + this case, being logged in and having the necessary payload.
  6. +
  7. If the intention meets all requirements we then fire the addToChecklist + action.
  8. +
  9. Material is added to the users checklist.
  10. +
+

The difference between the two might seem superfluous but the important +distinction to make is that with our current implementation we are not able to +serialize and persist the actions as the application state across page loads. By +defining intent explicitly we are able to serialize it and persist it between +page loads.

+

This resolves in the implementation being able to rehydrate the persisted state, +look at the persisted intentions and have the individual application +implementations decide what to do with the intention.

+

A mock implementation of the case by case business logic looks as follows.

+
const initialStore = {
+  authenticated: false,
+  intent: {
+    status: '',
+    payload: {}
+  }
+}
+
+const fulfillAction = store.authenticated &&
+    (store.intent.status === 'pending' || store.intent.status === 'tried')
+const getRequirements = !store.authenticated && store.intent.status === 'pending'
+const abandonIntention = !store.authenticated && store.intent.status === 'tried'
+
+function AddToChecklist ({ materialId, store }) {
+  useEffect(() => {
+    if (fulfillAction) {
+      // We fire the actual functionality required to add a material to the
+      // checklist and we remove the intention as a result of it being
+      // fulfilled.
+      addToChecklistAction(store, materialId)
+    } else if (getRequirements) {
+      // Before we redirect we set the status to be "tried".
+      redirectToLogin(store)
+    } else if (abandonIntention) {
+      // We abandon the intent so that we won't have an infinite loop of retries
+      // at every page load.
+      abandonAddToChecklistIntention(store)
+    }
+  }, [materialId, store.intent.status])
+  return (
+    <button
+      onClick={() => {
+        // We do not fire the actual logic that is required to add a material to
+        // the checklist. Instead we add the intention of said action to the
+        // store. This is when we would set the status of the intent to pending
+        // and provide the payload.
+        addToChecklistIntention(store, materialId)
+      }}
+    >
+      Tilføj til huskeliste
+    </button>
+  )
+}
+
+

We utilize session storage to persist the state on the client due to it's short +lived nature and porous features.

+

We choose Redux as the framework to implemenent this. Redux is a blessed choice +in this instance. It has widespread use, an approachable design and is +well-documented. The best way to go about a current Redux implementation as of +now is @reduxjs/toolkit. Redux is a +sufficiently advanced framework to support other uses of application state and +even co-locating shared state between applications.

+

For our persistence concerns we want to use the most commonly used tool for +that, redux-persist. There are some +implementation details +to take into consideration when integrating the two.

+

Alternatives considered

+

Persistence in URL

+

We could persist the intentions in the URL that is delivered back to the client +after a page reload. This would still imply some of the architectural decisions +described in Decision in regards to having an "intent" state, but some of the +different status flags etc. would not be needed since state is virtually shared +across page loads in the url. However this simpler solution cannot handle more +complex situations than what can be described in the URL feasibly.

+

useContext

+

React offers useContext() +for state management as an alternative to Redux.

+

We prefer Redux as it provides a more complete environment when working with +state management. There is already a community of established practices and +libraries which integrate with Redux. One example of this is our need to persist +actions. When using Redux we can handle this with redux-persist. With +useContext() we would have to roll our own implementation.

+

Some of the disadvantages of using Redux e.g. the amount of required boilerplate +code are addressed by using @reduxjs/toolkit.

+

Status

+

Accepted

+

Consequences

+
    +
  • We are able to support most if not all of our rehydration cases and therefore + pick up user flow from where we left it.
  • +
  • Heavy degree of complexity is added to tasks that requires an intention + instead of a simple action.
  • +
  • Saving the immediate state to the session storage makes for yet another place + to "clear cache".
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/architecture/adr-002-ui-text-handling/index.html b/DPL-React/architecture/adr-002-ui-text-handling/index.html new file mode 100644 index 0000000..ce49003 --- /dev/null +++ b/DPL-React/architecture/adr-002-ui-text-handling/index.html @@ -0,0 +1,3229 @@ + + + + + + + + + + + + + + + + + + + + + + + UI Text Handling - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

UI Text Handling

+

Context

+

It has been decided that app context/settings should be passed from the +server side rendered mount points via data props. +One type of settings is text strings that is defined by +the system/host rendering the mount points. +Since we are going to have quite some levels of nested components +it would be nice to have a way to extract the string +without having to define them all the way down the tree.

+

Decision

+

A solution has been made that extracts the props holding the strings +and puts them in the Redux store under the index: text at the app entry level. +That is done with the help of the withText() High Order Component. +The solution of having the strings in redux +enables us to fetch the strings at any point in the tree. +A hook called: useText() makes it simple to request a certain string +inside a given component.

+

Alternatives considered

+

One major alternative would be not doing it and pass down the props. +But that leaves us with text props all the way down the tree +which we would like to avoid. +Some translation libraries has been investigated +but that would in most cases give us a lot of tools and complexity +that is not needed in order to solve the relatively simple task.

+

Consequences

+

Since we omit the text props down the tree +it leaves us with fewer props and a cleaner component setup. +Although some "magic" has been introduced +with text prop matching and storage in redux, +it is outweighed by the simplicity of the HOC wrapper and useText hook.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/architecture/adr-003-downshift/index.html b/DPL-React/architecture/adr-003-downshift/index.html new file mode 100644 index 0000000..ccb3b75 --- /dev/null +++ b/DPL-React/architecture/adr-003-downshift/index.html @@ -0,0 +1,3294 @@ + + + + + + + + + + + + + + + + + + + + + + + Downshift - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Downshift

+

Context

+

As a part of the project, we need to implement a dropdown autosuggest component. +This component needs to be complient with modern website accessibility rules:

+
    +
  • The component dropdown items are accessible by keyboard by using arrow keys - +down and up.
  • +
  • The items visibly change when they are in current focus.
  • +
  • Items are selectable using the keyboard.
  • +
+

Apart from these accessibility features, the component needs to follow a somewhat +complex design described in +this Figma file. +As visible in the design this autosuggest dropdown doesn't consist only of single +line text items, but also contains suggestions for specific works - utilizing +more complex suggestion items with cover pictures, and release years.

+

Decision

+

Our research on the most popular and supported javascript libraries heavily leans +on this specific article. +In combination with our needs described above in the context section, but also +considering what it would mean to build this component from scratch without any +libraries, the decision taken favored a library called +Downshift.

+

This library is the second most popular JS library used to handle autsuggest +dropdowns, multiselects, and select dropdowns with a large following and +continuous support. Out of the box, it follows the +ARIA principles, and +handles problems that we would normally have to solve ourselves (e.g. opening and +closing of the dropdown/which item is currently in focus/etc.).

+

Another reason why we choose Downshift over its peer libraries is the amount of +flexibility that it provides. In our eyes, this is a strong quality of the library +that allows us to also implement more complex suggestion dropdown items.

+

Alternatives considered

+

Building the autosuggest dropdown not using javascript libraries

+

In this case, we would have to handle accessibility and state management of the +component with our own custom solutition.

+

Status

+

Accepted.

+

Consequences

+
    +
  • We are able to comply with ARIA accesibility design principles for autosuggest +dropdowns/comboboxes.
  • +
  • We introduced complexity to the project for initial project integration of the +library.
  • +
  • After initial integration, this library can be utilized for all other select, +multiselect, and autosuggest/combobox solutions.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/architecture/adr-004-relative-ci/index.html b/DPL-React/architecture/adr-004-relative-ci/index.html new file mode 100644 index 0000000..f10d8d5 --- /dev/null +++ b/DPL-React/architecture/adr-004-relative-ci/index.html @@ -0,0 +1,3307 @@ + + + + + + + + + + + + + + + + + + + + + + + RelativeCI - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

RelativeCI

+

Context

+

Staying informed about how the size of the JavaScript we require browsers to +download to use the project plays an important part in ensuring a performant +solution.

+

We currently have no awareness of this in this project and the result surfaces +down the line when the project is integrated with the CMS, which is +tested with Lighthouse.

+

To address this we want a solution that will help us monitor the changes to the +size of the bundle we ship for each PR.

+

Decision

+

We add integration to RelativeCI to the project. +RelativeCI supports our primary use case and has a number of qualities which we +value:

+
    +
  • Support for GitHub actions and reporting as GitHub status checks
  • +
  • Support for fork-based development workflows
  • +
  • A free tier for open source projects
  • +
  • Other types of analysis e.g. duplicate packages, continual monitoring
  • +
+

Alternatives considered

+

Bundlewatch

+

Bundlewatch and its ancestor, bundlesize +combine a CLI tool and a web app to provide bundle analysis and feedback on +GitHub as status checks.

+

These solutions no longer seem to be actively maintained. There are several +bugs that would affect +us and fixes remain unmerged. The project relies on a custom secret instead of +GITHUB_TOKEN. This makes supporting our fork-based development workflow +harder.

+

Bundle comparison

+

This is a GitHub Action which can be used in configurations where statistics +for two bundles are compared e.g. for the base and head of a pull request. This +results in a table of changes displayed as a comment in the pull request. +This is managed using GITHUB_TOKEN.

+

Status

+

Accepted.

+

Consequences

+
    +
  • We can determine the effect of adding a new JavaScript library to our project
  • +
  • We add another dependency to a third party system
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/architecture/adr-005-react-use/index.html b/DPL-React/architecture/adr-005-react-use/index.html new file mode 100644 index 0000000..54b54cb --- /dev/null +++ b/DPL-React/architecture/adr-005-react-use/index.html @@ -0,0 +1,3226 @@ + + + + + + + + + + + + + + + + + + + + + + + React Use - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

React Use

+

Context

+

The decision of obtaining react-use as a part of the project originated +from the problem that arose from having an useEffect hook with +an object as a dependency.

+

useEffect does not support comparison of objects or arrays and we needed +a method for comparing such natives.

+

Decision

+

We decided to go for the react-use package +react-use. +The reason is threefold:

+
    +
  • It could solve the problem with deep comparison of dependencies by using + useDeepCompareEffect
  • +
  • It offered an alternative to the + react-hook-inview viewport handling. + So we did not need to use two packages.
  • +
  • It has a range of other utility hooks that we can make use of in the future.
  • +
+

Alternatives considered

+

We could have used our own implementation of the problem. +But since it is a common problem we might as well use a community backed solution. +And react-use gives us a wealth of other tools.

+

Consequences

+

We can now use useDeepCompareEffect instead of useEffect +in cases where we have arrays or objects amomg the dependencies. +And we can make use of all the other utility hooks that the package provides.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/architecture/adr-006-unit-tests/index.html b/DPL-React/architecture/adr-006-unit-tests/index.html new file mode 100644 index 0000000..5afb31b --- /dev/null +++ b/DPL-React/architecture/adr-006-unit-tests/index.html @@ -0,0 +1,3225 @@ + + + + + + + + + + + + + + + + + + + + + + + Unit Tests - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Unit Tests

+

Context

+

The code base is growing and so does the number of functions and custom hooks.

+

While we have a good coverage in our UI tests from Cypress we are lacking +something to tests the inner workings of the applications.

+

With unit tests added we can test bits of functionality that is shared +between different areas of the application and make sure that we get the +expected output giving different variations of input.

+

Decision

+

We decided to go for Vitest which is an easy to use and +very fast unit testing tool.

+

It has more or less the same capabilities as Jest +which is another popular testing framework which is similar.

+

Vitest is framework agnostic so in order to make it possible to test hooks +we found @testing-library/react-hooks +that works in conjunction with Vitest.

+

Alternatives considered

+

We could have used Jest. But trying that we experienced major problems +with having both Jest and Cypress in the same codebase. +They have colliding test function names and Typescript could not figure it out.

+

There is probably a solution but at the same time we got Vitest recommended. +It seemed very fast and just as capable as Jest. And we did not have the +colliding issues of shared function/object names.

+

Consequences

+

We now have unit test as a part of the mix which brings more stability +and certainty that the individual pieces of the application work.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/architecture/adr-007-configuration/index.html b/DPL-React/architecture/adr-007-configuration/index.html new file mode 100644 index 0000000..d5df58c --- /dev/null +++ b/DPL-React/architecture/adr-007-configuration/index.html @@ -0,0 +1,3278 @@ + + + + + + + + + + + + + + + + + + + + + + + Configuration - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Configuration

+

Context

+

This project provides a set of React applications which can be placed in a +mounting application to provide self service features for Danish public +libraries. To support variations in the appearance and behavior of these +applications, the applications support configuration. +In practice these are provided through data attributes on the root element of +the application set by the mounting application.

+

A configuration value can use one of three different types:

+
    +
  1. String value
  2. +
  3. Multiple string values
  4. +
  5. A JSON object
  6. +
+

Also, a configuration value may be required or optional for the application to +function.

+

Our use of TypeScript should match our handling of configuration.

+

Practice has shown that string values are relatively easy to handle but JSON +values are not. This leads to errors which can be hard to debug. Consequently, +we need to clarify our handling of configuration and especially for JSON values.

+

Decision

+

We will use the following rules for handling configuration:

+
    +
  1. The mounting application is responsible for providing configuration values + in the correct format.
  2. +
  3. If a configuration value is optional and does not have a value then the + corresponding data attribute must not be set by the mounting application.
  4. +
  5. If a configuration value is optional the application can alternately specify + a required configuration value with an enabled boolean property. If no + configuration is provided { enabled: false } is an acceptable value.
  6. +
+

Alternatives considered

+

Make configuration optional

+

We could make all configuration optional and introduce suitable handling if no +value is provided. This could introduce default values, errors or workarounds.

+

This would make the React application code more complex. We would rather push +this responsibility to the mounting application.

+

Consequences

+

This approach provides a first step for improving our handling of +configuration. Potential improvements going forward are:

+
    +
  1. Runtime validation of JSON values using libraries like Zod, + io-ts or Runtypes.
  2. +
  3. Specification of configuration values in a separate file which can be used + by the mounting application to provide configuration values and by the + React application to validate the provided values. One format for doing so + would be JSON Schema which is widely supported in JavaScript, TypeScript and + PHP (used by DPL CMS).
  4. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/architecture/adr-008-error-handling/index.html b/DPL-React/architecture/adr-008-error-handling/index.html new file mode 100644 index 0000000..b71b852 --- /dev/null +++ b/DPL-React/architecture/adr-008-error-handling/index.html @@ -0,0 +1,3275 @@ + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Error handling in the React Apps - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Error handling in the React Apps

+

Context

+

We needed to handle errors thrown in the apps, both in requests by fetchers but +also exceptions thrown at runtime.

+

Errors were already handled in the initial implementation of this project. +An Error Boundary +was already implemented but we were lacking two important features:

+
    +
  • Every app shouldn't show its error in their own scope. We wanted to centralise +the error rendering for the end user
  • +
  • All errors should NOT be caught by the Error Boundary an thereby block the +whole app.
  • +
+

Decision

+

Show the errors in one place

+

To solve the problem with each app showing its own error, we decided to make use +of React's Portal system. +The host (most likely dpl-cms) that includes the apps tells the apps via a +config data prop what the container id of error wrapper is. Then the Error +boundary system makes sure to use that id when rendering the error.

+

Handle errors differently depending on type

+

Each app is wrapped with an Error Boundary. In the initial implementation +that meant that if any exception was thrown the Error Boundary would catch +any exception and showing an error message to the end user. +Furthermore the error boundary makes sure the errors are being logged to error.log.

+

Exceptions can be thrown in the apps at runtime both as a result +of a failing request to a service or on our side. +The change made to the error system in this context was to distinguish +between the request errors. +Some data for some services are being considered to be essential for the apps to +work, others are not. +To make sure that not all fetching errors are being caught we have created a +queryErrorHandler in src/components/store.jsx. The queryErrorHandler looks +at the type of error/instance of error that is being thrown +and decides if the Error Boundary should be used or not. +At the moment of this writing there are two type of errors: critical and non-critical. +The critical ones are being caught by the Error Boundary and the non-critical +are only ending up in the error log and are not blocking the app.

+

Consequences

+

By using the Portal system we have solved the problem about showing multiple +errors instead of a single global one.

+

By choosing to distinguish different error types by looking at their instance name +we can decide which fetch errors should be blocking the app and which should not. +In both cases the errors are being logged and we can trace them in our logs.

+

Alternatives considered

+

None.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/architecture/index.html b/DPL-React/architecture/index.html new file mode 100644 index 0000000..12d6276 --- /dev/null +++ b/DPL-React/architecture/index.html @@ -0,0 +1,3087 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture decision records - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture decision records

+

We document decisions regarding the architecture of the project using ADRs.

+

We follow the format suggested by Michael Nygaard +containing the sections: Title, Context, Decision and Consequences.

+

We do not use the Status section. If an ADR is merged into the main branch +it is by default accepted.

+

We have added an Alternatives considered section to ensure we document +alternative solutions and the pros and cons for these.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/campaigns/index.html b/DPL-React/campaigns/index.html new file mode 100644 index 0000000..43f1940 --- /dev/null +++ b/DPL-React/campaigns/index.html @@ -0,0 +1,3250 @@ + + + + + + + + + + + + + + + + + + + + + + + Campaigns - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + +

Campaigns

+

Campaigns are elements that are shown on the search result page above the search +result list. There are three types of campaigns:

+
    +
  1. Full campaigns - containing an image and a some text.
  2. +
  3. Text-only campaigns - they don't show any images.
  4. +
  5. Image-only campaigns - they don't show any text.
  6. +
+

However, they are only shown in case certain criteria are met. We check for this +by contacting the dpl-cms API.

+

How campaign setup works in dpl-cms

+

Dpl-cms is a cms system based on Drupal, where the system administrators can set +up campaigns they want to show to their users. Drupal also allows the cms system +to act as an API endpoint that we then can contact from our apps.

+

The cms administrators can specify the content (image, text) and the visibility +criteria for each campaign they create. The visibility criteria is based on +search filter facets. +Does that sound familiar? Yes, we use another API to get that very data +in THIS project - in the search result app. +The facets differ based on the search string the user uses for their search.

+

As an example, the dpl-cms admin could wish to show a Harry Potter related +campaign to all the users whose search string retreives search facets which +have "Harry Potter" as one of the most relevant subjects. +Campaigns in dpl-cms can use triggers such as subject, main language, etc.

+

React code example

+

An example code snippet for retreiving a campaign from our react apps would then +look something like this:

+
  // Creating a state to store the campaign data in.
+  const [campaignData, setCampaignData] = useState<CampaignMatchPOST200 | null>(
+    null
+  );
+
+  // Retreiving facets for the campaign based on the search query and existing
+  // filters.
+  const { facets: campaignFacets } = useGetFacets(q, filters);
+
+  // Using the campaign hook generated by Orval from src/core/dpl-cms/dpl-cms.ts
+  // in order to get the mutate function that lets us retreive campaigns.
+  const { mutate } = useCampaignMatchPOST();
+
+  // Only fire the campaign data call if campaign facets or the mutate function
+  // change their value.
+  useDeepCompareEffect(() => {
+    if (campaignFacets) {
+      mutate(
+        {
+          data: campaignFacets as CampaignMatchPOSTBodyItem[],
+           params: {
+            _format: "json"
+          }
+        },
+        {
+          onSuccess: (campaign) => {
+            setCampaignData(campaign);
+          },
+          onError: () => {
+            // Handle error.
+          }
+        }
+      );
+    }
+  }, [campaignFacets, mutate]);
+
+

Showing campaigns in dpl-react when in development mode

+

You first need to make sure to have a campaign set up in your locally running +dpl-cms ( +run this repo locally) +Then, in order to see campaigns locally in dpl-react in development mode, you +will most likely need a browser plugin such as Google Chrome's +"Allow CORS: Access-Control-Allow-Origin" +in order to bypass CORS policy for API data calls.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/code_guidelines/index.html b/DPL-React/code_guidelines/index.html new file mode 100644 index 0000000..b9374d4 --- /dev/null +++ b/DPL-React/code_guidelines/index.html @@ -0,0 +1,3859 @@ + + + + + + + + + + + + + + + + + + + + + + + React Code guidelines - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

React Code guidelines

+

The following guidelines describe best practices for developing code for React +components for the Danish Public Libraries CMS project. The guidelines should +help achieve:

+
    +
  • A stable, secure and high quality foundation for building and maintaining + client-side TypeScript components for library websites
  • +
  • Consistency across multiple developers participating in the project
  • +
  • The best possible conditions for sharing components between library websites
  • +
  • The best possible conditions for the individual library website to customize + configuration and appearance
  • +
+

Contributions to the DPL React project will be reviewed by members of the Core +team. These guidelines should inform contributors about what to expect in such a +review. If a review comment cannot be traced back to one of these guidelines it +indicates that the guidelines should be updated to ensure transparency.

+

Coding standards

+

The project follows the Airbnb JavaScript Style Guide +and Airbnb React/JSX Style Guide. They +have been extended to support TypeScript.

+

This choice is based on multiple factors:

+
    +
  1. Historically the community of developers working with the Danish Public + Libraries has ties to the Drupal project. + Drupal has adopted the Airbnb JavaScript Style Guide + so this choice should ensure consistency between the two projects.
  2. +
  3. Airbnb's standard is one of the best known and most used in the JavaScript + coding standard landscape.
  4. +
  5. Airbnb’s standard is both comprehensive and well documented.
  6. +
  7. Airbnb’s standards cover both JavaScript in general React/JSX specifically. + This avoids potential conflicts between multiple standards.
  8. +
+

The following lists significant areas where the project either intentionally +expands or deviates from the official standards or areas which developers should +be especially aware of.

+

General

+
    +
  • The default language for all code and comments is English.
  • +
  • Components must be compatible with the latest stable version of the following + browsers:
  • +
  • Desktop
      +
    • Microsoft Edge
    • +
    • Google Chrome
    • +
    • Safari
    • +
    • Firefox
    • +
    +
  • +
  • Mobile
      +
    • Google Chrome
    • +
    • Safari
    • +
    • Firefox
    • +
    • Samsung Browser
    • +
    +
  • +
+

TypeScript

+

Named functions vs. anonymous arrow functions

+

AirBnB's only guideline towards this is that +anonymous arrow function nation is preferred over the normal anonymous function +notation.

+

This project sticks to the above guideline as well. If we need to pass a +function as part of a callback or in a promise chain and we on top of that need +to pass some contextual variables that are not passed implicitly from either the +callback or the previous link in the promise chain we want to make use of an +anonymous arrow function as our default.

+

This comes with the build in disclaimer that if an anonymous function isn't +required the implementer should heavily consider moving the logic out into its +own named function expression.

+

The named function is primarily desired due to it's easier to debug nature in +stacktraces.

+

React

+
    +
  • Configuration must be passed as props for components. This allows the host + system to modify how a component works when it is inserted.
  • +
  • All components should be provided with skeleton screens. + This ensures that the user interface reflects the final state even when data + is loaded asynchronously. This reduces load time frustration.
  • +
  • Components should be optimistic. + Unless we have reason to believe that an operation may fail we should provide + fast response to users.
  • +
  • All interface text must be implemented as props for components. This allows + the host system to provide a suitable translation/version when using the + component.
  • +
+

CSS

+
    +
  • All classes must have the dpl- prefix. This makes them distinguishable from + classes provided by the host system.
  • +
  • Class names should follow the Block-Element-Modifier architecture.
  • +
  • Components must use and/or provide a default style sheet which at least + provides a minimum of styling showing the purpose of the component.
  • +
  • Elements must be provided with meaningful classes even though they are not + targeted by the default style sheet. This helps host systems provide + additional styling of the components. Consider how the component consists of + blocks and elements with modifiers and how these can be nested within each + other.
  • +
  • Components must use SCSS for styling. The project uses PostCSS + and PostCSS-SCSS within Webpack for + processing.
  • +
+

HTML

+
    +
  • Components must use semantic HTML5 markup.
  • +
  • Components must provide configuration to set a top headline level for the + component. This helps provide a proper document outline to ensure the + accessibility of the system.
  • +
+

Naming

+

Files

+

Files provided by components must be placed in the following folders and have +the extensions defined here.

+
    +
  • Components (React applications)
  • +
  • apps/[component-name]/[component-name].tsx
      +
    • Core TSX component.
    • +
    +
  • +
  • components/[component-name]/[component-name].scss
      +
    • Stylesheet for the component.
    • +
    +
  • +
  • apps/[component-name]/[component-name].entry.tsx
      +
    • Main application entrypoint.
    • +
    • This will usually also be where state management is implemented.
    • +
    • This must not include the default stylesheet.
    • +
    +
  • +
  • apps/[component-name]/[component-name].dev.tsx
      +
    • Storybook entry for the component.
    • +
    • If the component has a stylesheet this must also be included here.
    • +
    +
  • +
  • apps/[component-name]/[component-name].mount.ts
      +
    • Code for registering the application to be booted when a page is loaded on + the host system.
    • +
    +
  • +
  • apps/[component-name]/[component-name].test.ts
      +
    • Test of the component implemented with Cypress
    • +
    +
  • +
  • Reusable elements (React components)
  • +
  • components/[component-name]/[component-name].dev.tsx
  • +
  • components/[component-name]/[component-name].tsx
  • +
  • components/[component-name]/[component-name].scss
  • +
  • Reusable functions and classes
  • +
  • core/[function].ts
  • +
  • core/[Class].ts
  • +
+

Third party code

+

The project uses Yarn as a package +manager to handle code which is developed outside the project repository. Such +code must not be committed to the Core project repository.

+

When specifying third party package versions the project follows these +guidelines:

+
    +
  • Use the ^ next significant release operator + for packages which follow semantic versioning.
  • +
  • The version specified must be the latest known working and secure version. We + do not want accidental downgrades.
  • +
  • We want to allow easy updates to all working releases within the same major + version.
  • +
  • Packages which are not intended to be executed at runtime in the production + environment should be marked as development dependencies.
  • +
+

Reusing dependencies

+

Components must reuse existing dependencies in the project before adding new +ones which provide similar functionality. This ensures consistency and avoids +unnecessary increases in the package size of the project.

+

The reasoning behind the choice of key dependencies have been documented in +the architecture directory.

+

Altering third party code

+

The project uses patches rather than forks to modify third party packages. This +makes maintenance of modified packages easier and avoids a collection of forked +repositories within the project.

+
    +
  • Use an appropriate method for the corresponding package manager for managing + the patch.
  • +
  • Patches should be external by default. In rare cases it may be needed to + commit them as a part of the project.
  • +
  • When providing a patch you must document the origin of the patch e.g. through + an url in a commit comment or preferably in the package manager configuration + for the project.
  • +
+

Code comments

+

Code comments which describe what an implementation does should only be used +for complex implementations usually consisting of multiple loops, conditional +statements etc.

+

Inline code comments should focus on why an unusual implementation has been +implemented the way it is. This may include references to such things as +business requirements, odd system behavior or browser inconsistencies.

+

Commit messages

+

Commit messages in the version control system help all developers understand the +current state of the code base, how it has evolved and the context of each +change. This is especially important for a project which is expected to have a +long lifetime.

+

Commit messages must follow these guidelines:

+
    +
  1. Each line must not be more than 72 characters long
  2. +
  3. The first line of your commit message (the subject) must contain a short + summary of the change. The subject should be kept around 50 characters long.
  4. +
  5. The subject must be followed by a blank line
  6. +
  7. Subsequent lines (the body) should explain what you have changed and why the + change is necessary. This provides context for other developers who have not + been part of the development process. The larger the change the more + description in the body is expected.
  8. +
  9. If the commit is a result of an issue in a public issue tracker, + platform.dandigbib.dk, then the subject must start with the issue number + followed by a colon (:). If the commit is a result of a private issue tracker + then the issue id must be kept in the commit body.
  10. +
+

When creating a pull request the pull request description should not contain any +information that is not already available in the commit messages.

+

Developers are encouraged to read How to Write a Git Commit Message by Chris Beams.

+

Tool support

+

The project aims to automate compliance checks as much as possible using static +code analysis tools. This should make it easier for developers to check +contributions before submitting them for review and thus make the review process +easier.

+

The following tools pay a key part here:

+
    +
  1. Eslint with the following rulesets and plugins:
      +
    1. Airbnb JavaScript Style Guide
    2. +
    3. Airbnb React/JSX Style Guide
    4. +
    5. Prettier
    6. +
    7. Cypress
    8. +
    +
  2. +
  3. Stylelint with the following rulesets and plugins
      +
    1. Recommended SCSS
    2. +
    3. Prettier
    4. +
    5. BEM support
    6. +
    +
  4. +
+

In general all tools must be able to run locally. This allows developers to get +quick feedback on their work.

+

Tools which provide automated fixes are preferred. This reduces the burden of +keeping code compliant for developers.

+

Code which is to be exempt from these standards must be marked accordingly in +the codebase - usually through inline comments (Eslint, +Stylelint). This must also +include a human readable reasoning. This ensures that deviations do not affect +future analysis and the project should always pass through static analysis.

+

If there are discrepancies between the automated checks and the standards +defined here then developers are encouraged to point this out so the automated +checks or these standards can be updated accordingly.

+

Writing frontend tests

+

The frontend tests are executed in +Cypress.

+

The test files are placed alongside the application components +and are named following pattern: "*.test.ts". Eg.: material.test.ts.

+

Test structuring

+

After quite a lot of bad experiences with unstable tests +and reading both the official documentation +and articles about the best practices we have ended up with a recommendation of +how to write the tests.

+

According to this article +it is important to distinguish between commands and assertions. +Commands are used in the beginning of a statement and yields a chainable element +that can be followed by one or more assertions in the end.

+

So first we target an element. +Next we can make one or more assertions on the element.

+

We have created some helper commands for targeting an element: +getBySel, getBySelLike and getBySelStartEnd. +They look for elements as advised by the +Selecting Elements +section from the Cypress documentation about best practices.

+

Example of a statement:

+
// Targeting.
+cy.getBySel("reservation-success-title-text")
+  // Assertion.
+  .should("be.visible")
+  // Another assertion.
+  .and("contain", "Material is available and reserved for you!");
+
+

Writing Unit Tests

+

We are using Vitest as framework for running unit tests. +By using that we can test functions (and therefore also hooks) and classes.

+

Where do I place my tests?

+

They have to be placed in src/tests/unit.

+

Or they can also be placed next to the code at the end of a file as described +here.

+
export const sum = (...numbers: number[]) =>
+  numbers.reduce((total, number) => total + number, 0);
+
+if (import.meta.vitest) {
+  const { describe, expect, it } = import.meta.vitest;
+
+  describe("sum", () => {
+    it("should sum numbers", () => {
+      expect(sum(1, 2, 3)).toBe(6);
+    });
+  });
+}
+
+

In that way it helps us to test and mock unexported functions.

+

Testing hooks

+

For testing hooks we are using the library +@testing-library/react-hooks +and you can also take a look at the text test +to see how it can be done.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/error_handling/index.html b/DPL-React/error_handling/index.html new file mode 100644 index 0000000..7f226cc --- /dev/null +++ b/DPL-React/error_handling/index.html @@ -0,0 +1,3306 @@ + + + + + + + + + + + + + + + + + + + + + + + Error Handling - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Error Handling

+

Error handling is something that is done on multiple levels: +Eg.: Form validation, network/fetch error handling, runtime errors. +You could also argue that the fact that the codebase is making use of typescript +and code generation from the various http services (graphql/REST) belongs to +the same idiom of error handling in order to make the applications more robust.

+

Error Boundary

+

Error boundary was introduced +in React 16 and makes it possible to implement a "catch all" feature +catching "uncatched" errors and replacing the application with a component +to the users that something went wrong. +It is meant ato be a way of having a safety net and always be able to tell +the end user that something went wrong. +The apps are being wrapped in the error boundary handling which makes it +possible to catch thrown errors at runtime.

+

Fetch and Error Boundary

+

Async operations and therby also fetch are not being handled out of the box +by the React Error Boundary. But fortunately react-query, which is being used +both by the REST services (Orval) and graphql (Graphql Code Generator), has a +way of addressing the problem. The QueryClient can be configured to trigger +the Error Boundary system +if an error is thrown. +So that is what we are doing.

+

Fetch error classes

+

Two different types of error classes have been made in order to handle errors +in the fetchers: http errors and fetcher errors.

+

Http errors are the ones originating from http errors +and have a status code attached.

+

Fetcher errors are whatever else bad that could apart from http errors. +Eg. JSON parsing gone wrong.

+

Both types of errors comes in two variants: "normal" and "critical". The idea is +that only critical errors will trigger an Error Boundary.

+

For instance if you look at the +DBC Gateway fetcher +it throws a DbcGateWayHttpError in case of a http error occurs. +DbcGateWayHttpError +extends the +FetcherCriticalHttpError +which makes sure to trigger the Error Boundary system.

+
Using Error.name
+

The reason why *.name is set in the errors is to make it clear which error +was thrown. If we don't do that the name of the parent class is used in the +error log. And then it is more difficult to trace where the error originated +from.

+

Future considerations

+

The initial implementation is handling http errors on a coarse level: +If response.ok is false then throw an error. If the error is critical +the error boundary is triggered. +In future version you could could take more things into consideration +regarding the error:

+
    +
  • Should all status codes trigger an error?
  • +
  • Should we have different types of error level depending on request +and/or http method?
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/fbs-adapter-client/index.html b/DPL-React/fbs-adapter-client/index.html new file mode 100644 index 0000000..615d9df --- /dev/null +++ b/DPL-React/fbs-adapter-client/index.html @@ -0,0 +1,3257 @@ + + + + + + + + + + + + + + + + + + + + + + + FBS adapter client - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

FBS adapter client

+

The FBS client adapter is autogenerated base the swagger 1.2 json files from +FBS. But Orval requires that we use swagger version 2.0 and the adapter has some +changes in paths and parameters. So some conversion is need at the time of this +writing.

+

FBS documentation can be found here.

+

All this will hopefully be changed when/or if the adapter comes with its own +specifications.

+

API spec converter

+

A repository dpl-fbs-adapter-tool +tool build around PHP and NodeJS can translate the FBS specifikation into one +usable for Orval client generator. It also filters out all the FBS calls not +need by the DPL project.

+

The tool uses go-task to simply the execution of the +command.

+

Setup

+

Simple use the installation task.

+
task dev:install
+
+

Convert swagger

+

First convert the swagger 1.2 (located in /fbs/externalapidocs) to swagger 2.0 +using the api-spec-converter tool.

+
dev:swagger2yaml
+
+

Build the Adapter specifications

+

Build the swagger specification usable by Orval then run Orval.

+
task dev:convert
+
+

FBS Adapter

+

The FSB adapter lives at: https://github.com/DBCDK/fbs-cms-adapter

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/images/wiremock-studio-stub.png b/DPL-React/images/wiremock-studio-stub.png new file mode 100644 index 0000000..66f8c1a Binary files /dev/null and b/DPL-React/images/wiremock-studio-stub.png differ diff --git a/DPL-React/index.html b/DPL-React/index.html new file mode 100644 index 0000000..a9b2920 --- /dev/null +++ b/DPL-React/index.html @@ -0,0 +1,3934 @@ + + + + + + + + + + + + + + + + + + + + + + + DPL React - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

DPL React

+

A set of React components and applications providing self-service features for +Danish public libraries.

+

Development

+

Requirements

+ +

Before you can install the project you need to create the file ~/.npmrc to +access the GitHub package registry as described using a personal access token. +The token must be created with the required scopes: repo and read:packages

+

If you have npm installed locally this can be achieved by running the following +command and using the token when prompted for password.

+
npm login --registry=https://npm.pkg.github.com
+
+

Howto

+
    +
  1. Ensure that your Node version matches what is specified in .nvmrc.
  2. +
  3. Run task dev:start
  4. +
  5. Storybook will open automatically in a browser
  6. +
  7. The console will contain build and lint information
  8. +
  9. If you need to log in through Adgangsplatformen:
  10. +
  11. Add 127.0.0.1 dpl-react.docker to your /etc/hosts file
  12. +
  13. Ensure that Node can bind to port 80
  14. +
  15. Use http://dpl-react.docker/ instead of e.g. http://localhost:8080
  16. +
  17. If you want to use Wiremock instead of production systems run + task dev:mocks:start
  18. +
+

Step Debugging in Visual Studio Code (no docker)

+

If you want to enable step debugging you need to:

+
    +
  • Copy .vscode.example/launch.json into .vscode/
  • +
  • Mark 1 or more breakpoints on a line in the left gutter on an open file
  • +
  • In the top menu in VS Code choose: Run -> Start Debugging
  • +
  • Type in your user password if ask to
  • +
  • Start debugging 🤖∿💻
  • +
+

Access tokens

+

Access token must be retrieved from Adgangsplatformen, +a single sign-on solution for public libraries in Denmark, and OpenPlatform, +an API for danish libraries.

+

Usage of these systems require a valid client id and secret which must be +obtained from your library partner or directly from DBC, the company responsible +for running Adgangsplatformen and OpenPlatform.

+

This project include a client id that matches the storybook setup which can be +used for development purposes. You can use the /auth story to sign in to +Adgangsplatformen for the storybook context.

+

(Note: if you enter Adgangsplatformen again after signing it, you will get +signed out, and need to log in again. This is not a bug, as you stay logged +in otherwise.)

+

Library token

+

To test the apps that is indifferent to wether the user is authenticated or not +it is possible to set a library token via the library component in Storybook. +Workflow:

+
    +
  • Retrieve a library token via OpenPlatform
  • +
  • Insert the library token in the Library Token story in storybook
  • +
+

Standard and style

+

JavaScript + JSX

+

For static code analysis we make use of the Airbnb JavaScript Style Guide +and for formatting we make use of Prettier +with the default configuration. The above choices have been influenced by a +multitude of factors:

+
    +
  • Historically Drupal core have been making use of the Airbnb JavaScript Style + Guide.
  • +
  • Airbnb's standard is comparatively the best known + and one of the most used + in the JavaScript coding standard landscape.
  • +
+

This makes future adoption easier for onboarding contributors and support is to +be expected for a long time.

+
Named functions Vs. Anonymous arrow functions
+

AirBnB's only guideline towards this is that anonymous arrow function are +preferred over the normal anonymous function notation.

+

When you must use an anonymous function (as when passing an inline callback), +use arrow function notation.

+
+

Why? It creates a version of the function that executes in the context of +this, which is usually what you want, and is a more concise syntax.

+

Why not? If you have a fairly complicated function, you might move that logic +out into its own named function expression.

+
+

Reference

+

This project stick to the above guideline as well. If we need to pass a function +as part of a callback or in a promise chain and we on top of that need to pass +some contextual variables that is not passed implicit from either the callback +or the previous link in the promise chain we want to make use of an anonymous +arrow function as our default.

+

This comes with the build in disclaimer that if an anonymous function isn't +required the implementer should heavily consider moving the logic out into it's +own named function expression.

+

The named function is primarily desired due to it's easier to debug nature in +stacktraces.

+

Create a new application

+
+ 1. Create a new application component + +
// ./src/apps/my-new-application/my-new-application.jsx
+import React from "react";
+import PropTypes from "prop-types";
+
+export function MyNewApplication({ text }) {
+  return (
+      <h2>{text}</h2>
+  );
+}
+
+MyNewApplication.defaultProps = {
+  text: "The fastest man alive!"
+};
+
+MyNewApplication.propTypes = {
+  text: PropTypes.string
+};
+
+export default MyNewApplication;
+
+ +
+ +
+ 2. Create the entry component + +
// ./src/apps/my-new-application/my-new-application.entry.jsx
+import React from "react";
+import PropTypes from "prop-types";
+import MyNewApplication from "./my-new-application";
+
+// The props of an entry is all of the data attributes that were
+// set on the DOM element. See the section on "Naive app mount." for
+// an example.
+export function MyNewApplicationEntry(props) {
+  return <MyNewApplication text='Might be from a server?' />;
+}
+
+export default MyNewApplicationEntry;
+
+ +
+ +
+ 3. Create the mount + +
// ./src/apps/my-new-application/my-new-application.mount.js
+import addMount from "../../core/addMount";
+import MyNewApplication from "./my-new-application.entry";
+
+addMount({ appName: "my-new-application", app: MyNewApplication });
+
+ +
+ +
+ 4. Add a story for local development + +
// ./src/apps/my-new-application/my-new-application.dev.jsx
+import React from "react";
+import MyNewApplicationEntry from "./my-new-application.entry";
+import MyNewApplication from "./my-new-application";
+
+export default { title: "Apps|My new application" };
+
+export function Entry() {
+  // Testing the version that will be shipped.
+  return <MyNewApplicationEntry />;
+}
+
+export function WithoutData() {
+  // Play around with the application itself without server side data.
+  return <MyNewApplication />;
+}
+
+ +
+ +
+ 5. Run the development environment + +
  yarn dev
+
+ +OR depending on your dev environment (docker or not) + +
  sudo yarn dev
+
+ +
+ +

Voila! You browser should have opened and a storybook environment is ready +for you to tinker around.

+

Application state-machine

+

Most applications will have multiple internal states, so to aid consistency, +it's recommended to:

+
  const [status, setStatus] = useState("<initial state>");
+
+

and use the following states where appropriate:

+

initial: Initial state for applications that require some sort of +initialization, such as making a request to see if a material can be ordered, +before rendering the order button. Errors in initialization can go directly to +the failed state, or add custom states for communication different error +conditions to the user. Should render either nothing or as a +skeleton/spinner/message.

+

ready: The general "ready state". Applications that doesn't need +initialization (a generic button for instance) can use ready as the initial +state set in the useState call. This is basically the main waiting state.

+

processing: The application is taking some action. For buttons this will be +the state used when the user has clicked the button and the application is +waiting for reply from the back end. More advanced applications may use it while +doing backend requests, if reflecting the processing in the UI is desired. +Applications using optimistic feedback will render this state the same as the +finished state.

+

failed: Processing failed. The application renders an error message.

+

finished: End state for one-shot actions. Communicates success to the user.

+

Applications can use additional states if desired, but prefer the above if +appropriate.

+

Style your application

+
+ 1. Create an application specific stylesheet + +
// ./src/apps/my-new-application/my-new-application.scss
+.dpl-warm {
+  color: maroon;
+}
+
+ +
+ +
+ 2. Add the class to your application + +
// ./src/apps/my-new-application/my-new-application.jsx
+import React from "react";
+import PropTypes from "prop-types";
+
+export function MyNewApplication({ text }) {
+  return (
+      <h2 className='warm'>{text}</h2>
+  );
+}
+
+MyNewApplication.defaultProps = {
+  text: "The fastest man alive!"
+};
+
+MyNewApplication.propTypes = {
+  text: PropTypes.string
+};
+
+export default MyNewApplication;
+
+ +
+ +
+ 3. Import the scss into your story + +
// ./src/apps/my-new-application/my-new-application.dev.jsx
+import React from "react";
+import MyNewApplicationEntry from "./my-new-application.entry";
+import MyNewApplication from "./my-new-application";
+
+import './my-new-application.scss';
+
+export default { title: "Apps|My new application" };
+
+export function Entry() {
+  // Testing the version that will be shipped.
+  return <MyNewApplicationEntry />;
+}
+
+export function WithoutData() {
+  // Play around with the application itself without server side data.
+  return <MyNewApplication />;
+}
+
+ +
+ +

Cowabunga! You now got styling in your application

+

Style using the DPL design system

+

This project includes styling created by its sister repository - +the design system +as a npm package.

+

By default the project should include a release of the design system matching +the current state of the project.

+

To update the design system to the latest stable release of the design system +run:

+
yarn add @danskernesdigitalebibliotek/dpl-design-system@latest
+
+

This command installs the latest released version of the package. Whenever a +new version of the design system package is released, it is necessary +to reinstall the package in this project using the same command to get the +newest styling, because yarn adds a specific version number to the package name +in package.json.

+

Using unreleased design

+

If you need to work with published but unreleased code from a specific branch +of the design system, you can also use the branch name as the tag for the npm +package, replacing all special characters with dashes (-).

+

Example: To use the latest styling from a branch in the design system called +feature/availability-label, run:

+
yarn add @danskernesdigitalebibliotek/dpl-design-system@feature-availability-label
+
+

If the branch resides in a fork (usually before a pull request is merged) you +can use aliasing +and run:

+
yarn config set "@my-fork:registry" "https://npm.pkg.github.com"
+yarn add @danskernesdigitalebibliotek/dpl-design-system@npm:@my-fork/dpl-design-system@feature-availability-label
+
+

If the branch is updated and you want the latest changes to take effect locally +update the release used:

+
yarn upgrade @danskernesdigitalebibliotek/dpl-design-system
+
+

Note that references to unreleased code should never make it into official +versions of the project.

+

Cross application components

+

If the component is simple enough to be a primitive you would use in multiple +occasions it's called an 'atom'. Such as a button or a link. If it's more +specific that that and to be used across apps we just call it a component. An +example would be some type of media presented alongside a header and some text.

+

The process when creating an atom or a component is more or less similar, but +some structural differences might be needed.

+

Creating an atom

+
+ 1. Create the atom + +
// ./src/components/atoms/my-new-atom/my-new-atom.jsx
+import React from "react";
+import PropTypes from 'prop-types';
+
+/**
+ * A simple button.
+ *
+ * @export
+ * @param {object} props
+ * @returns {ReactNode}
+ */
+export function MyNewAtom({ className, children }) {
+  return <button className={`btn ${className}`}>{children}</button>;
+}
+
+MyNewAtom.propTypes = {
+  className: PropTypes.string,
+  children: PropTypes.node.isRequired
+}
+
+MyNewAtom.defaultProps = {
+  className: ""
+}
+
+export default MyNewAtom;
+
+ +
+ +
+ 2. Create styles for the atom + +
// ./src/components/atoms/my-new-atom/my-new-atom.scss
+.dpl-btn {
+    color: blue;
+}
+
+ +
+ +
+ 3. Import the atom's styles into the component stylesheet + +
// ./src/components/components.scss
+@import 'atoms/button/button.scss';
+@import 'atoms/my-new-atom/my-new-atom.scss';
+
+ +
+ +
+ 4. Create a story for your atom + +
// ./src/components/atoms/my-new-atom/my-new-atom.dev.jsx
+import React from "react";
+import MyNewAtom from "./my-new-atom";
+
+export default { title: "Atoms|My new atom" };
+
+export function WithText() {
+  return <MyNewAtom>Cick me!</MyNewAtom>;
+}
+
+ +
+ +
+ 5. Import the atom into the applications or other components where +you would want to use it + +
// ./src/apps/my-new-application/my-new-application.jsx
+import React, {Fragment} from "react";
+import PropTypes from "prop-types";
+
+import MyNewAtom from "../../components/atom/my-new-atom/my-new-atom"
+
+export function MyNewApplication({ text }) {
+  return (
+      <Fragment>
+        <h2 className='warm'>{text}</h2>
+        <MyNewAtom className='additional-class' />
+      </Fragment>
+  );
+}
+
+MyNewApplication.defaultProps = {
+  text: "The fastest man alive!"
+};
+
+MyNewApplication.propTypes = {
+  text: PropTypes.string
+};
+
+export default MyNewApplication;
+
+ +
+ +

Finito! You now know how to share code across applications

+

Creating a component

+

Repeat all of the same steps as with an atom but place it in it's own directory +inside components.

+

Such as ./src/components/my-new-component/my-new-component.jsx

+

Editor example configuration

+

If you use Code we provide some easy to +use and nice defaults for this project. They are located in .vscode.example. +Simply rename the directory from .vscode.example to .vscode and you are good +to go. This overwrites your global user settings for this workspace and suggests +som extensions you might want.

+

Usage

+

There are two ways to use the components provided by this project:

+
    +
  1. As standalone JavaScript applications mounted within HTML pages generated by + a separate system.
  2. +
  3. As components within a larger JavaScript application (Under development)
  4. +
+

Naive app mount

+

So let's say you wanted to make use of an application within an existing HTML +page such as what might be generated serverside by platforms like Drupal, +WordPress etc.

+

For this use case you should download the dist.zip package from +the latest release of the project +and unzip somewhere within the web root of your project. The package contains a +set of artifacts needed to use one or more applications within an HTML page.

+
+ HTML Example + +A simple example of the required artifacts and how they are used looks like +this: + +
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Naive mount</title>
+    <!-- Include CSS files to provide default styling -->
+    <link rel="stylesheet" href="/dist/components.css">
+</head>
+<body>
+    <b>Here be dragons!</b>
+    <!-- Data attributes will be camelCased on the react side aka.
+         props.errorText and props.text -->
+    <div data-dpl-app='add-to-checklist' data-text="Chromatic dragon"
+         data-error-text="Minor mistake"></div>
+    <div data-dpl-app='a-none-existing-app'></div>
+
+    <!-- Load order og scripts is of importance here -->
+    <script src="/dist/runtime.js"></script>
+    <script src="/dist/bundle.js"></script>
+    <script src="/dist/mount.js"></script>
+    <!-- After the necessary scripts you can start loading applications -->
+    <script src="/dist/add-to-checklist.js"></script>
+    <script>
+      // For making successful requests to the different services we need one or
+      // more valid tokens.
+     window.dplReact.setToken("user","XXXXXXXXXXXXXXXXXXXXXX");
+     window.dplReact.setToken("library","YYYYYYYYYYYYYYYYYYYYYY");
+
+      // If this function isn't called no apps will display.
+      // An app will only be displayed if there is a container for it
+      // and a corresponding application loaded.
+      window.dplReact.mount(document);
+    </script>
+</body>
+</html>
+
+ +
+ +

As a minimum you will need the runtime.js and bundle.js. For styling +of atoms and components you will need to import components.css.

+

Each application also has its own JavaScript artifact and it might have a CSS +artifact as well. Such as add-to-checklist.js and add-to-checklist.css.

+

To mount the application you need an HTML element with the correct data +attribute.

+
<div data-dpl-app='add-to-checklist'></div>
+
+

The name of the data attribute should be data-dpl-app and the value should be +the name of the application - the value of the appName parameter assigned in +the application .mount.js file.

+

Data attributes and props

+

As stated above, every application needs the corresponding data-dpl-app +attribute to even be mounted and shown on the page. Additional data attributes +can be passed if necessary. Examples would be contextual ids etc. Normally these +would be passed in by the serverside platform e.g. Drupal, Wordpress etc.

+
<div data-dpl-app='add-to-checklist' data-id="870970-basis:54172613"
+     data-error-text="A mistake was made"></div>
+
+

The above data-id would be accessed as props.id and data-error-text as +props.errorText in the entrypoint of an application.

+
+ Example + +
// ./src/apps/my-new-application/my-new-application.entry.jsx
+import React from "react";
+import PropTypes from "prop-types";
+import MyNewApplication from './my-new-application.jsx';
+
+export function MyNewApplicationEntry({ id }) {
+  return (
+    <MyNewApplication
+      // 870970-basis:54172613
+      id={id}
+    />
+}
+
+export default MyNewApplicationEntry;
+
+ +
+ +

To fake this in our development environment we need to pass these same data +attributes into our entrypoint.

+
+ Example + +
// ./src/apps/my-new-application/my-new-application.dev.jsx
+import React from "react";
+import MyNewApplicationEntry from "./my-new-application.entry";
+import MyNewApplication from "./my-new-application";
+
+export default { title: "Apps|My new application" };
+
+export function Entry() {
+  // Testing the version that will be shipped.
+  return <MyNewApplicationEntry id="870970-basis:54172613" />;
+}
+
+export function WithoutData() {
+  // Play around with the application itself without server side data.
+  return <MyNewApplication />;
+}
+
+ +
+ +

Extending the project

+

If you want to extend this project - either by introducing new components or +expand the functionality of the existing ones - and your changes can be +implemented in a way that is valuable to users in general, please submit pull +requests.

+

Even if that is not the case and you have special needs the infrastructure of +the project should also be helpful to you.

+

In such a situation you should fork this project and extend it to your own needs +by implementing new applications. New applications +can reuse various levels of infrastructure provided by the project such as:

+
    +
  1. Integration with various webservices
  2. +
  3. User authentication and token management
  4. +
  5. Visual atoms or components
  6. +
  7. Visual representations of existing applications
  8. +
  9. Styling using SCSS
  10. +
  11. Test infrastructure
  12. +
  13. Application mounting
  14. +
+

Once the customization is complete the result can be packaged for distribution +by pushing the changes to the forked repository:

+
    +
  1. Changes pushed to the master branch of the forked repository will + automatically update the latest release of the fork.
  2. +
  3. Tags pushed to the forked repository also will be published as new releases + in the fork.
  4. +
+

The result can be used in the same ways as the original project.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/request_mocking_wiremock/index.html b/DPL-React/request_mocking_wiremock/index.html new file mode 100644 index 0000000..fb1dfb0 --- /dev/null +++ b/DPL-React/request_mocking_wiremock/index.html @@ -0,0 +1,3268 @@ + + + + + + + + + + + + + + + + + + + + + + + Request mocking / Wiremock - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Request mocking / Wiremock

+

We use Wiremock to enable request mocking and +reduce dependency on external systems during development.

+

Background

+

The React components generally work with data from external systems. Having +data in a specific state may be necessary in the development of some features +but it can be cumbersome so set up exactly such a state. Many factors can also +reduce the usefulness of this state. Development may change it once a +reservation is deleted. Time affects where a loan is current or overdue.

+

With all this in mind it is useful for us to be able to recreate specific +states. We do so using Wiremock. Wiremock +is a system which allows us to mock external APIs. It can be configured or +instrumented to return predefined responses given predefined requests.

+

We use Wiremock through Docker based on the following setup:

+
    +
  1. One Wiremock instance per external API we want to mock
  2. +
  3. Wiremock Studio provides a UI for managing + Wiremock instances
  4. +
  5. Mocked requests/responses are persisted as files in the repository for + sharing between developers
  6. +
  7. Wiremock is exposed to local browsers through a Docker DNS proxy like Dory
  8. +
  9. Storybook can be preconfigured to use Wiremock instead of production + webservices in .env
  10. +
+

Howtos

+

Use Wiremock instead of production services during development

+
    +
  1. Start Wiremock Docker containers: docker compose up -d
  2. +
  3. If available: Enable a Docker DNS proxy like Dory: dory up
  4. +
  5. Create/update a .env file with hostnames (and ports if necessary) for + Wiremock Docker containers e.g.:
  6. +
+
PUBLIZON_BASEURL=http://publizon-mock.docker`
+FBS_BASEURL=http://fbs-mock.docker`
+CMS_BASEURL=http://cms-mock.docker
+
+ +
    +
  1. Start Storybook: yarn run dev
  2. +
+

Set up a mocked response in Wiremock

+

To set up a mocked response for a new request to FBS do the following:

+
    +
  1. Open Wiremock Studio at http://dpl-mock.docker/
  2. +
  3. Click "FBS" to manage the Wiremock instance for FBS
  4. +
  5. Click "Stubs" to see a list of existing requests/responses
  6. +
  7. Click "New" to create a new request/response set and provide a name
  8. +
  9. Provide the HTTP method, path and other parts of the request to match (note: +make sure to check "advanced" option out, and match either path, or path AND the +query.)
  10. +
  11. Provide the response HTTP status code and body to return
  12. +
  13. Click "Save"
  14. +
  15. See that the stub has been persisted as a new file in + .docker/wiremock/fbs/mappings
  16. +
  17. Restart Wiremock docker images to load the updated stub
  18. +
+

The following example shows how one might create a mock which returns an error +if the client tries to delete a specific reservation.

+

Example stub in Wiremock Studio

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/skeleton_screens/index.html b/DPL-React/skeleton_screens/index.html new file mode 100644 index 0000000..18fadf4 --- /dev/null +++ b/DPL-React/skeleton_screens/index.html @@ -0,0 +1,3226 @@ + + + + + + + + + + + + + + + + + + + + + + + Skeleton screens - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Skeleton screens

+

In order to improve both UX and the performance score you can choose to use +skeleton screens in situation where you need to fill the interface +with data from a requests to an external service.

+

Main Purpose

+

The skeleton screens are being showed instantly in order to deliver +some content to the end user fast while loading data. +When the data is arriving the skeleton screens are being replaced +with the real data.

+

How to use it

+

The skeleton screens are rendered with help from the +skeleton-screen-css library. + By using ssc classes + you can easily compose screens + that simulate the look of a "real" rendering with real data.

+

Example

+

In this example we are showing a search result item as a skeleton screen. +The skeleton screen consists of a cover, a headline and two lines of text. +In this case we wanted to maintain the styling of the .card-list-item +wrapper. And show the skeleton screen elements by using ssc classes.

+

```tsx +import React from "react";

+

const SearchResultListItemSkeleton: React.FC = () => { + return ( +

+
 
+
+
+
 
+
 
+
+
+ ); +};

+

export default SearchResultListItemSkeleton;

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/DPL-React/ui_text_handling/index.html b/DPL-React/ui_text_handling/index.html new file mode 100644 index 0000000..3a69f3a --- /dev/null +++ b/DPL-React/ui_text_handling/index.html @@ -0,0 +1,3342 @@ + + + + + + + + + + + + + + + + + + + + + + + UI Text Handling - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

UI Text Handling

+

This document describes how to use the text functionality +that is partly defined in src/core/utils/text.tsx and in src/core/text.slice.ts.

+

Main Purpose

+

The main purpose of the functionality is to be able to access strings defined +at app level inside of sub components +without passing them all the way down via props. +You can read more about the decision +and considerations here.

+

How to use it

+

In order to use the system the component that has the text props +needs to be wrapped with the withText high order function. +The texts can hereafter be accessed by using the useText hook.

+

Simple example

+

In this example we have a HelloWorld app with three text props attached:

+
import React from "react";
+import { withText } from "../../core/utils/text";
+import HelloWorld from "./hello-world";
+
+export interface HelloWorldEntryProps {
+  titleText: string;
+  introductionText: string;
+  whatText: string;
+}
+
+const HelloWorldEntry: React.FC<HelloWorldEntryProps> = (
+  props: HelloWorldEntryProps
+) => <HelloWorld />;
+
+export default withText(HelloWorldEntry);
+
+

Now it is possible to access the strings like this:

+
import * as React from "react";
+import { Hello } from "../../components/hello/hello";
+import { useText } from "../../core/utils/text";
+
+const HelloWorld: React.FC = () => {
+  const t = useText();
+  return (
+    <article>
+      <h2>{t("titleText")}</h2>
+      <p>{t("introductionText")}</p>
+      <p>
+        <Hello shouldBeEmphasized />
+      </p>
+    </article>
+  );
+};
+export default HelloWorld;
+
+

Placeholder example

+

It is also possible to use placeholders in the text strings. +They can be handy when you want dynamic values embedded in the text.

+

A classic example is the welcome message to the authenticated user. +Let's say you have a text with the key: welcomeMessageText. +The value from the data prop is: Welcome @username, today is @date. +You would the need to reference it like this:

+
import * as React from "react";
+import { useText } from "../../core/utils/text";
+
+const HelloUser: React.FC = () => {
+  const t = useText();
+  const username = getUsername();
+  const currentDate = getCurrentDate();
+
+  const message = t("welcomeMessageText", {
+    placeholders: {
+      "@user": username,
+      "@date": currentDate
+    }
+  });
+
+  return (
+    <div>{message}</div>
+  );
+};
+export default HelloUser;
+
+

Plural example

+

Sometimes you want two versions of a text be shown +depending on if you have one or multiple items being referenced in the text.

+

That can be accommodated by using the plural text definition.

+

Let's say that an authenticated user has a list of unread messages in an inbox. +You could have a text key called: inboxStatusText. +The value from the data prop is:

+
{"type":"plural","text":["You have 1 message in the inbox",
+"You have @count messages in the inbox"]}.
+
+

You would then need to reference it like this:

+
import * as React from "react";
+import { useText } from "../../core/utils/text";
+
+const InboxStatus: React.FC = () => {
+  const t = useText();
+  const user = getUser();
+  const inboxMessageCount = getUserInboxMessageCount(user);
+
+  const status = t("inboxStatusText", {
+    count: inboxMessageCount,
+    placeholders: {
+      "@count": inboxMessageCount
+    }
+  });
+
+  return (
+    <div>{status}</div>
+    // If count == 1 the texts will be:
+    // "You have 1 message in the inbox"
+
+    // If count == 5  the texts will be:
+    // "You have 5 messages in the inbox"
+  );
+};
+export default InboxStatus;
+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000..1cf13b9 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.a7c05c9e.min.js b/assets/javascripts/bundle.a7c05c9e.min.js new file mode 100644 index 0000000..31d7407 --- /dev/null +++ b/assets/javascripts/bundle.a7c05c9e.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Fi=Object.create;var gr=Object.defineProperty;var ji=Object.getOwnPropertyDescriptor;var Wi=Object.getOwnPropertyNames,Dt=Object.getOwnPropertySymbols,Ui=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty,no=Object.prototype.propertyIsEnumerable;var oo=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,R=(e,t)=>{for(var r in t||(t={}))xr.call(t,r)&&oo(e,r,t[r]);if(Dt)for(var r of Dt(t))no.call(t,r)&&oo(e,r,t[r]);return e};var io=(e,t)=>{var r={};for(var o in e)xr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Dt)for(var o of Dt(e))t.indexOf(o)<0&&no.call(e,o)&&(r[o]=e[o]);return r};var yr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Di=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Wi(t))!xr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=ji(t,n))||o.enumerable});return e};var Vt=(e,t,r)=>(r=e!=null?Fi(Ui(e)):{},Di(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var ao=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var co=yr((Er,so)=>{(function(e,t){typeof Er=="object"&&typeof so!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(H){return!!(H&&H!==document&&H.nodeName!=="HTML"&&H.nodeName!=="BODY"&&"classList"in H&&"contains"in H.classList)}function p(H){var mt=H.type,ze=H.tagName;return!!(ze==="INPUT"&&a[mt]&&!H.readOnly||ze==="TEXTAREA"&&!H.readOnly||H.isContentEditable)}function c(H){H.classList.contains("focus-visible")||(H.classList.add("focus-visible"),H.setAttribute("data-focus-visible-added",""))}function l(H){H.hasAttribute("data-focus-visible-added")&&(H.classList.remove("focus-visible"),H.removeAttribute("data-focus-visible-added"))}function f(H){H.metaKey||H.altKey||H.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(H){o=!1}function h(H){s(H.target)&&(o||p(H.target))&&c(H.target)}function w(H){s(H.target)&&(H.target.classList.contains("focus-visible")||H.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(H.target))}function A(H){document.visibilityState==="hidden"&&(n&&(o=!0),te())}function te(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function ie(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(H){H.target.nodeName&&H.target.nodeName.toLowerCase()==="html"||(o=!1,ie())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",A,!0),te(),r.addEventListener("focus",h,!0),r.addEventListener("blur",w,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var Yr=yr((Rt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Rt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Rt=="object"?Rt.ClipboardJS=r():t.ClipboardJS=r()})(Rt,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ii}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(_){return!1}}var h=function(_){var O=f()(_);return u("cut"),O},w=h;function A(V){var _=document.documentElement.getAttribute("dir")==="rtl",O=document.createElement("textarea");O.style.fontSize="12pt",O.style.border="0",O.style.padding="0",O.style.margin="0",O.style.position="absolute",O.style[_?"right":"left"]="-9999px";var j=window.pageYOffset||document.documentElement.scrollTop;return O.style.top="".concat(j,"px"),O.setAttribute("readonly",""),O.value=V,O}var te=function(_,O){var j=A(_);O.container.appendChild(j);var D=f()(j);return u("copy"),j.remove(),D},ie=function(_){var O=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},j="";return typeof _=="string"?j=te(_,O):_ instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(_==null?void 0:_.type)?j=te(_.value,O):(j=f()(_),u("copy")),j},J=ie;function H(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?H=function(O){return typeof O}:H=function(O){return O&&typeof Symbol=="function"&&O.constructor===Symbol&&O!==Symbol.prototype?"symbol":typeof O},H(V)}var mt=function(){var _=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},O=_.action,j=O===void 0?"copy":O,D=_.container,Y=_.target,ke=_.text;if(j!=="copy"&&j!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&H(Y)==="object"&&Y.nodeType===1){if(j==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(j==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(ke)return J(ke,{container:D});if(Y)return j==="cut"?w(Y):J(Y,{container:D})},ze=mt;function Ie(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(O){return typeof O}:Ie=function(O){return O&&typeof Symbol=="function"&&O.constructor===Symbol&&O!==Symbol.prototype?"symbol":typeof O},Ie(V)}function _i(V,_){if(!(V instanceof _))throw new TypeError("Cannot call a class as a function")}function ro(V,_){for(var O=0;O<_.length;O++){var j=_[O];j.enumerable=j.enumerable||!1,j.configurable=!0,"value"in j&&(j.writable=!0),Object.defineProperty(V,j.key,j)}}function Ai(V,_,O){return _&&ro(V.prototype,_),O&&ro(V,O),V}function Ci(V,_){if(typeof _!="function"&&_!==null)throw new TypeError("Super expression must either be null or a function");V.prototype=Object.create(_&&_.prototype,{constructor:{value:V,writable:!0,configurable:!0}}),_&&br(V,_)}function br(V,_){return br=Object.setPrototypeOf||function(j,D){return j.__proto__=D,j},br(V,_)}function Hi(V){var _=Pi();return function(){var j=Wt(V),D;if(_){var Y=Wt(this).constructor;D=Reflect.construct(j,arguments,Y)}else D=j.apply(this,arguments);return ki(this,D)}}function ki(V,_){return _&&(Ie(_)==="object"||typeof _=="function")?_:$i(V)}function $i(V){if(V===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return V}function Pi(){if(typeof Reflect=="undefined"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(V){return!1}}function Wt(V){return Wt=Object.setPrototypeOf?Object.getPrototypeOf:function(O){return O.__proto__||Object.getPrototypeOf(O)},Wt(V)}function vr(V,_){var O="data-clipboard-".concat(V);if(_.hasAttribute(O))return _.getAttribute(O)}var Ri=function(V){Ci(O,V);var _=Hi(O);function O(j,D){var Y;return _i(this,O),Y=_.call(this),Y.resolveOptions(D),Y.listenClick(j),Y}return Ai(O,[{key:"resolveOptions",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Ie(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function(ke){return Y.onClick(ke)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,ke=this.action(Y)||"copy",Ut=ze({action:ke,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Ut?"success":"error",{action:ke,text:Ut,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return w(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,ke=!!document.queryCommandSupported;return Y.forEach(function(Ut){ke=ke&&!!document.queryCommandSupported(Ut)}),ke}}]),O}(s()),Ii=Ri},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,h,w){var A=c.apply(this,arguments);return l.addEventListener(u,A,w),{destroy:function(){l.removeEventListener(u,A,w)}}}function p(l,f,u,h,w){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(A){return s(A,f,u,h,w)}))}function c(l,f,u,h){return function(w){w.delegateTarget=a(w.target,f),w.delegateTarget&&h.call(l,w)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,h,w){if(!u&&!h&&!w)throw new Error("Missing required arguments");if(!a.string(h))throw new TypeError("Second argument must be a String");if(!a.fn(w))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,h,w);if(a.nodeList(u))return l(u,h,w);if(a.string(u))return f(u,h,w);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,h,w){return u.addEventListener(h,w),{destroy:function(){u.removeEventListener(h,w)}}}function l(u,h,w){return Array.prototype.forEach.call(u,function(A){A.addEventListener(h,w)}),{destroy:function(){Array.prototype.forEach.call(u,function(A){A.removeEventListener(h,w)})}}}function f(u,h,w){return s(document.body,u,h,w)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var ts=/["'&<>]/;ei.exports=rs;function rs(e){var t=""+e,r=ts.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||s(u,h)})})}function s(u,h){try{p(o[u](h))}catch(w){f(i[0][3],w)}}function p(u){u.value instanceof nt?Promise.resolve(u.value.v).then(c,l):f(i[0][2],u)}function c(u){s("next",u)}function l(u){s("throw",u)}function f(u,h){u(h),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof de=="function"?de(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function k(e){return typeof e=="function"}function ft(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ft(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=de(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(A){t={error:A}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(k(l))try{l()}catch(A){i=A instanceof zt?A.errors:[A]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=de(f),h=u.next();!h.done;h=u.next()){var w=h.value;try{fo(w)}catch(A){i=i!=null?i:[],A instanceof zt?i=q(q([],N(i)),N(A.errors)):i.push(A)}}}catch(A){o={error:A}}finally{try{h&&!h.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)fo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Fe.EMPTY;function qt(e){return e instanceof Fe||e&&"closed"in e&&k(e.remove)&&k(e.add)&&k(e.unsubscribe)}function fo(e){k(e)?e():e.unsubscribe()}var $e={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var ut={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Fe(function(){o.currentObservers=null,qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Eo(r,o)},t}(F);var Eo=function(e){re(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){re(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var Lt={now:function(){return(Lt.delegate||Date).now()},delegate:void 0};var _t=function(e){re(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Lt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(vt);var So=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(gt);var Hr=new So(To);var Oo=function(e){re(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=bt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(bt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(vt);var Mo=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(gt);var me=new Mo(Oo);var M=new F(function(e){return e.complete()});function Yt(e){return e&&k(e.schedule)}function kr(e){return e[e.length-1]}function Xe(e){return k(kr(e))?e.pop():void 0}function He(e){return Yt(kr(e))?e.pop():void 0}function Bt(e,t){return typeof kr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return k(e==null?void 0:e.then)}function Jt(e){return k(e[ht])}function Xt(e){return Symbol.asyncIterator&&k(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Gi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Gi();function tr(e){return k(e==null?void 0:e[er])}function rr(e){return lo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return k(e==null?void 0:e.getReader)}function W(e){if(e instanceof F)return e;if(e!=null){if(Jt(e))return Ji(e);if(xt(e))return Xi(e);if(Gt(e))return Zi(e);if(Xt(e))return Lo(e);if(tr(e))return ea(e);if(or(e))return ta(e)}throw Zt(e)}function Ji(e){return new F(function(t){var r=e[ht]();if(k(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Xi(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,we(1),r?Be(t):zo(function(){return new ir}))}}function Fr(e){return e<=0?function(){return M}:x(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,h=0,w=!1,A=!1,te=function(){f==null||f.unsubscribe(),f=void 0},ie=function(){te(),l=u=void 0,w=A=!1},J=function(){var H=l;ie(),H==null||H.unsubscribe()};return x(function(H,mt){h++,!A&&!w&&te();var ze=u=u!=null?u:r();mt.add(function(){h--,h===0&&!A&&!w&&(f=Wr(J,p))}),ze.subscribe(mt),!l&&h>0&&(l=new at({next:function(Ie){return ze.next(Ie)},error:function(Ie){A=!0,te(),f=Wr(ie,n,Ie),ze.error(Ie)},complete:function(){w=!0,te(),f=Wr(ie,a),ze.complete()}}),W(H).subscribe(l))})(c)}}function Wr(e,t){for(var r=[],o=2;oe.next(document)),e}function $(e,t=document){return Array.from(t.querySelectorAll(e))}function P(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Re(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var xa=S(d(document.body,"focusin"),d(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Re()||document.body),B(1));function et(e){return xa.pipe(m(t=>e.contains(t)),K())}function kt(e,t){return C(()=>S(d(e,"mouseenter").pipe(m(()=>!0)),d(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Me(+!r*t)):le,Q(e.matches(":hover"))))}function Bo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Bo(e,r)}function E(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Bo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function wt(e){let t=E("script",{src:e});return C(()=>(document.head.appendChild(t),S(d(t,"load"),d(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),L(()=>document.head.removeChild(t)),we(1))))}var Go=new g,ya=C(()=>typeof ResizeObserver=="undefined"?wt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Go.next(t)))),v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),B(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return ya.pipe(y(r=>r.observe(t)),v(r=>Go.pipe(b(o=>o.target===t),L(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function Tt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Jo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ue(e){return{x:e.offsetLeft,y:e.offsetTop}}function Xo(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Zo(e){return S(d(window,"load"),d(window,"resize")).pipe(Le(0,me),m(()=>Ue(e)),Q(Ue(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function De(e){return S(d(e,"scroll"),d(window,"scroll"),d(window,"resize")).pipe(Le(0,me),m(()=>pr(e)),Q(pr(e)))}var en=new g,Ea=C(()=>I(new IntersectionObserver(e=>{for(let t of e)en.next(t)},{threshold:0}))).pipe(v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),B(1));function tt(e){return Ea.pipe(y(t=>t.observe(e)),v(t=>en.pipe(b(({target:r})=>r===e),L(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function tn(e,t=16){return De(e).pipe(m(({y:r})=>{let o=ce(e),n=Tt(e);return r>=n.height-o.height-t}),K())}var lr={drawer:P("[data-md-toggle=drawer]"),search:P("[data-md-toggle=search]")};function rn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function Ve(e){let t=lr[e];return d(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function wa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ta(){return S(d(window,"compositionstart").pipe(m(()=>!0)),d(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function on(){let e=d(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:rn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Re();if(typeof o!="undefined")return!wa(o,r)}return!0}),pe());return Ta().pipe(v(t=>t?M:e))}function xe(){return new URL(location.href)}function pt(e,t=!1){if(G("navigation.instant")&&!t){let r=E("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function nn(){return new g}function an(){return location.hash.slice(1)}function sn(e){let t=E("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Sa(e){return S(d(window,"hashchange"),e).pipe(m(an),Q(an()),b(t=>t.length>0),B(1))}function cn(e){return Sa(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function $t(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function pn(){let e=matchMedia("print");return S(d(window,"beforeprint").pipe(m(()=>!0)),d(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():M))}function zr(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function Ne(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),B(1))}function ln(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),B(1))}function mn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),B(1))}function fn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function un(){return S(d(window,"scroll",{passive:!0}),d(window,"resize",{passive:!0})).pipe(m(fn),Q(fn()))}function dn(){return{width:innerWidth,height:innerHeight}}function hn(){return d(window,"resize",{passive:!0}).pipe(m(dn),Q(dn()))}function bn(){return z([un(),hn()]).pipe(m(([e,t])=>({offset:e,size:t})),B(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(Z("size")),n=z([o,r]).pipe(m(()=>Ue(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function Oa(e){return d(e,"message",t=>t.data)}function Ma(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function vn(e,t=new Worker(e)){let r=Oa(t),o=Ma(t),n=new g;n.subscribe(o);let i=o.pipe(X(),ne(!0));return n.pipe(X(),Pe(r.pipe(U(i))),pe())}var La=P("#__config"),St=JSON.parse(La.textContent);St.base=`${new URL(St.base,xe())}`;function Te(){return St}function G(e){return St.features.includes(e)}function ye(e,t){return typeof t!="undefined"?St.translations[e].replace("#",t.toString()):St.translations[e]}function Se(e,t=document){return P(`[data-md-component=${e}]`,t)}function ae(e,t=document){return $(`[data-md-component=${e}]`,t)}function _a(e){let t=P(".md-typeset > :first-child",e);return d(t,"click",{once:!0}).pipe(m(()=>P(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function gn(e){if(!G("announce.dismiss")||!e.childElementCount)return M;if(!e.hidden){let t=P(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),_a(e).pipe(y(r=>t.next(r)),L(()=>t.complete()),m(r=>R({ref:e},r)))})}function Aa(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function xn(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Aa(e,t).pipe(y(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))}function Pt(e,t){return t==="inline"?E("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},E("div",{class:"md-tooltip__inner md-typeset"})):E("div",{class:"md-tooltip",id:e,role:"tooltip"},E("div",{class:"md-tooltip__inner md-typeset"}))}function yn(...e){return E("div",{class:"md-tooltip2",role:"tooltip"},E("div",{class:"md-tooltip2__inner md-typeset"},e))}function En(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return E("aside",{class:"md-annotation",tabIndex:0},Pt(t),E("a",{href:r,class:"md-annotation__index",tabIndex:-1},E("span",{"data-md-annotation-id":e})))}else return E("aside",{class:"md-annotation",tabIndex:0},Pt(t),E("span",{class:"md-annotation__index",tabIndex:-1},E("span",{"data-md-annotation-id":e})))}function wn(e){return E("button",{class:"md-clipboard md-icon",title:ye("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,E("del",null,c)," "],[]).slice(0,-1),i=Te(),a=new URL(e.location,i.base);G("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=Te();return E("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},E("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&E("div",{class:"md-search-result__icon md-icon"}),r>0&&E("h1",null,e.title),r<=0&&E("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return E("span",{class:`md-tag ${c}`},p)}),o>0&&n.length>0&&E("p",{class:"md-search-result__terms"},ye("search.result.term.missing"),": ",...n)))}function Tn(e){let t=e[0].score,r=[...e],o=Te(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreqr(l,1)),...p.length?[E("details",{class:"md-search-result__more"},E("summary",{tabIndex:-1},E("div",null,p.length>0&&p.length===1?ye("search.result.more.one"):ye("search.result.more.other",p.length))),...p.map(l=>qr(l,1)))]:[]];return E("li",{class:"md-search-result__item"},c)}function Sn(e){return E("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>E("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Qr(e){let t=`tabbed-control tabbed-control--${e}`;return E("div",{class:t,hidden:!0},E("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function On(e){return E("div",{class:"md-typeset__scrollwrap"},E("div",{class:"md-typeset__table"},e))}function Ca(e){let t=Te(),r=new URL(`../${e.version}/`,t.base);return E("li",{class:"md-version__item"},E("a",{href:`${r}`,class:"md-version__link"},e.title))}function Mn(e,t){return e=e.filter(r=>{var o;return!((o=r.properties)!=null&&o.hidden)}),E("div",{class:"md-version"},E("button",{class:"md-version__current","aria-label":ye("select.version")},t.title),E("ul",{class:"md-version__list"},e.map(Ca)))}var Ha=0;function ka(e){let t=z([et(e),kt(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Jo(e)).pipe(oe(De),ct(1),m(()=>Xo(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function $a(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ha++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(X(),ne(!1)).subscribe(a);let s=a.pipe(Ht(c=>Me(+!c*250,Hr)),K(),v(c=>c?r:M),y(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>kt(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),ee(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),h=u.width/2;if(l.role==="tooltip")return{x:h,y:8+u.height};if(u.y>=f.height/2){let{height:w}=ce(l);return{x:h,y:-16-w}}else return{x:h,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),ee(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(P(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),be(me),ee(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ka(e).pipe(y(c=>i.next(c)),L(()=>i.complete()),m(c=>R({ref:e},c)))})}function lt(e,{viewport$:t},r=document.body){return $a(e,{content$:new F(o=>{let n=e.title,i=yn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Pa(e,t){let r=C(()=>z([Zo(e),De(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),we(+!o||1/0))))}function Ln(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(U(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),S(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Le(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),d(n,"click").pipe(U(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),d(n,"mousedown").pipe(U(a),ee(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Re())==null||c.blur()}}),r.pipe(U(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Pa(e,t).pipe(y(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function Ra(e){return e.tagName==="CODE"?$(".c, .c1, .cm",e):[e]}function Ia(e){let t=[];for(let r of Ra(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function _n(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Ia(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,En(p,i)),s.replaceWith(a.get(p)))}return a.size===0?M:C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=[];for(let[l,f]of a)c.push([P(".md-typeset",f),P(`:scope > li:nth-child(${l})`,e)]);return o.pipe(U(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?_n(f,u):_n(u,f)}),S(...[...a].map(([,l])=>Ln(l,t,{target$:r}))).pipe(L(()=>s.complete()),pe())})}function An(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return An(t)}}function Cn(e,t){return C(()=>{let r=An(e);return typeof r!="undefined"?fr(r,e,t):M})}var Hn=Vt(Yr());var Fa=0;function kn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return kn(t)}}function ja(e){return ge(e).pipe(m(({width:t})=>({scrollable:Tt(e).width>t})),Z("scrollable"))}function $n(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(Fr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Hn.default.isSupported()&&(e.closest(".copy")||G("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Fa++}`;let l=wn(c.id);c.insertBefore(l,e),G("content.tooltips")&&a.push(lt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=kn(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||G("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(U(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:M)))}}return $(":scope > span[id]",e).length&&e.classList.add("md-code__content"),ja(e).pipe(y(c=>n.next(c)),L(()=>n.complete()),m(c=>R({ref:e},c)),Pe(...a))});return G("content.lazy")?tt(e).pipe(b(n=>n),we(1),v(()=>o)):o}function Wa(e,{target$:t,print$:r}){let o=!0;return S(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),y(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Pn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),Wa(e,t).pipe(y(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}var Rn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Br,Da=0;function Va(){return typeof mermaid=="undefined"||mermaid instanceof Element?wt("https://unpkg.com/mermaid@10/dist/mermaid.min.js"):I(void 0)}function In(e){return e.classList.remove("mermaid"),Br||(Br=Va().pipe(y(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Rn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),B(1))),Br.subscribe(()=>ao(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Da++}`,r=E("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Br.pipe(m(()=>({ref:e})))}var Fn=E("table");function jn(e){return e.replaceWith(Fn),Fn.replaceWith(On(e)),I({ref:e})}function Na(e){let t=e.find(r=>r.checked)||e[0];return S(...e.map(r=>d(r,"change").pipe(m(()=>P(`label[for="${r.id}"]`))))).pipe(Q(P(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Wn(e,{viewport$:t,target$:r}){let o=P(".tabbed-labels",e),n=$(":scope > input",e),i=Qr("prev");e.append(i);let a=Qr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(X(),ne(!0));z([s,ge(e)]).pipe(U(p),Le(1,me)).subscribe({next([{active:c},l]){let f=Ue(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let h=pr(o);(f.xh.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([De(o),ge(o)]).pipe(U(p)).subscribe(([c,l])=>{let f=Tt(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),S(d(i,"click").pipe(m(()=>-1)),d(a,"click").pipe(m(()=>1))).pipe(U(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(U(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=P(`label[for="${c.id}"]`);l.replaceChildren(E("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),d(l.firstElementChild,"click").pipe(U(p),b(f=>!(f.metaKey||f.ctrlKey)),y(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return G("content.tabs.link")&&s.pipe(Ce(1),ee(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let w of $("[data-tabs]"))for(let A of $(":scope > input",w)){let te=P(`label[for="${A.id}"]`);if(te!==c&&te.innerText.trim()===f){te.setAttribute("data-md-switching",""),A.click();break}}window.scrollTo({top:e.offsetTop-u});let h=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...h])])}}),s.pipe(U(p)).subscribe(()=>{for(let c of $("audio, video",e))c.pause()}),tt(e).pipe(v(()=>Na(n)),y(c=>s.next(c)),L(()=>s.complete()),m(c=>R({ref:e},c)))}).pipe(Qe(se))}function Un(e,{viewport$:t,target$:r,print$:o}){return S(...$(".annotate:not(.highlight)",e).map(n=>Cn(n,{target$:r,print$:o})),...$("pre:not(.mermaid) > code",e).map(n=>$n(n,{target$:r,print$:o})),...$("pre.mermaid",e).map(n=>In(n)),...$("table:not([class])",e).map(n=>jn(n)),...$("details",e).map(n=>Pn(n,{target$:r,print$:o})),...$("[data-tabs]",e).map(n=>Wn(n,{viewport$:t,target$:r})),...$("[title]",e).filter(()=>G("content.tooltips")).map(n=>lt(n,{viewport$:t})))}function za(e,{alert$:t}){return t.pipe(v(r=>S(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function Dn(e,t){let r=P(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),za(e,t).pipe(y(n=>o.next(n)),L(()=>o.complete()),m(n=>R({ref:e},n)))})}var qa=0;function Qa(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?De(o):I({x:0,y:0}),i=S(et(t),kt(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ue(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Vn(e){let t=e.title;if(!t.length)return M;let r=`__tooltip_${qa++}`,o=Pt(r,"inline"),n=P(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),S(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Le(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Qa(o,e).pipe(y(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))}).pipe(Qe(se))}function Ka({viewport$:e}){if(!G("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Ye(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=Ve("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Nn(e,t){return C(()=>z([ge(e),Ka(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),B(1))}function zn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(X(),ne(!0));o.pipe(Z("active"),We(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue($("[title]",e)).pipe(b(()=>G("content.tooltips")),oe(a=>Vn(a)));return r.subscribe(o),t.pipe(U(n),m(a=>R({ref:e},a)),Pe(i.pipe(U(n))))})}function Ya(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),Z("active"))}function qn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?M:Ya(o,t).pipe(y(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))})}function Qn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),Z("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function Ba(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(oe(o=>d(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),B(1))}function Kn(e){let t=$("input",e),r=E("meta",{name:"theme-color"});document.head.appendChild(r);let o=E("meta",{name:"color-scheme"});document.head.appendChild(o);let n=$t("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),ee(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(be(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),Ba(t).pipe(U(n.pipe(Ce(1))),st(),y(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))})}function Yn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(y(o=>r.next({value:o})),L(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Gr=Vt(Yr());function Ga(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Bn({alert$:e}){Gr.default.isSupported()&&new F(t=>{new Gr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Ga(P(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(y(t=>{t.trigger.focus()}),m(()=>ye("clipboard.copied"))).subscribe(e)}function Gn(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function Ja(e,t){let r=new Map;for(let o of $("url",e)){let n=P("loc",o),i=[Gn(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of $("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(Gn(new URL(s),t))}}return r}function ur(e){return mn(new URL("sitemap.xml",e)).pipe(m(t=>Ja(t,new URL(e))),ve(()=>I(new Map)))}function Xa(e,t){if(!(e.target instanceof Element))return M;let r=e.target.closest("a");if(r===null)return M;if(r.target||e.metaKey||e.ctrlKey)return M;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):M}function Jn(e){let t=new Map;for(let r of $(":scope > *",e.head))t.set(r.outerHTML,r);return t}function Xn(e){for(let t of $("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function Za(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...G("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=Jn(document);for(let[o,n]of Jn(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return je($("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),M}),X(),ne(document))}function Zn({location$:e,viewport$:t,progress$:r}){let o=Te();if(location.protocol==="file:")return M;let n=ur(o.base);I(document).subscribe(Xn);let i=d(document.body,"click").pipe(We(n),v(([p,c])=>Xa(p,c)),pe()),a=d(window,"popstate").pipe(m(xe),pe());i.pipe(ee(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),S(i,a).subscribe(e);let s=e.pipe(Z("pathname"),v(p=>ln(p,{progress$:r}).pipe(ve(()=>(pt(p,!0),M)))),v(Xn),v(Za),pe());return S(s.pipe(ee(e,(p,c)=>c)),e.pipe(Z("pathname"),v(()=>e),Z("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),y(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",sn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),d(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(Z("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ri=Vt(ti());function oi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ri.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function It(e){return e.type===1}function dr(e){return e.type===3}function ni(e,t){let r=vn(e);return S(I(location.protocol!=="file:"),Ve("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:G("search.suggest")}}})),r}function ii({document$:e}){let t=Te(),r=Ne(new URL("../versions.json",t.base)).pipe(ve(()=>M)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>d(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),ee(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?M:(i.preventDefault(),I(p))}}return M}),v(i=>{let{version:a}=n.get(i);return ur(new URL(i)).pipe(m(s=>{let c=xe().href.replace(t.base,"");return s.has(c.split("#")[0])?new URL(`../${a}/${c}`,t.base):new URL(i)}))})))).subscribe(n=>pt(n,!0)),z([r,o]).subscribe(([n,i])=>{P(".md-header__topic").appendChild(Mn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ns(e,{worker$:t}){let{searchParams:r}=xe();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),Ve("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=xe();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=S(t.pipe(Ae(It)),d(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),B(1))}function ai(e,{worker$:t}){let r=new g,o=r.pipe(X(),ne(!0));z([t.pipe(Ae(It)),r],(i,a)=>a).pipe(Z("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(Z("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),d(e.form,"reset").pipe(U(o)).subscribe(()=>e.focus());let n=P("header [for=__search]");return d(n,"click").subscribe(()=>e.focus()),ns(e,{worker$:t}).pipe(y(i=>r.next(i)),L(()=>r.complete()),m(i=>R({ref:e},i)),B(1))}function si(e,{worker$:t,query$:r}){let o=new g,n=tn(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=P(":scope > :first-child",e),s=P(":scope > :last-child",e);Ve("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(ee(r),Ur(t.pipe(Ae(It)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?ye("search.result.none"):ye("search.result.placeholder");break;case 1:a.textContent=ye("search.result.one");break;default:let u=sr(l.length);a.textContent=ye("search.result.other",u)}});let p=o.pipe(y(()=>s.innerHTML=""),v(({items:l})=>S(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Ye(4),Vr(n),v(([f])=>f)))),m(Tn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(oe(l=>{let f=fe("details",l);return typeof f=="undefined"?M:d(f,"toggle").pipe(U(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(y(l=>o.next(l)),L(()=>o.complete()),m(l=>R({ref:e},l)))}function is(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=xe();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function ci(e,t){let r=new g,o=r.pipe(X(),ne(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),d(e,"click").pipe(U(o)).subscribe(n=>n.preventDefault()),is(e,t).pipe(y(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))}function pi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=S(d(n,"keydown"),d(n,"focus")).pipe(be(se),m(()=>n.value),K());return o.pipe(We(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(y(s=>o.next(s)),L(()=>o.complete()),m(()=>({ref:e})))}function li(e,{index$:t,keyboard$:r}){let o=Te();try{let n=ni(o.search,t),i=Se("search-query",e),a=Se("search-result",e);d(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Re();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of $(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,h])=>h-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...$(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Re()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=ai(i,{worker$:n});return S(s,si(a,{worker$:n,query$:s})).pipe(Pe(...ae("search-share",e).map(p=>ci(p,{query$:s})),...ae("search-suggest",e).map(p=>pi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ke}}function mi(e,{index$:t,location$:r}){return z([t,r.pipe(Q(xe()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>oi(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=E("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function as(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Jr(e,o){var n=o,{header$:t}=n,r=io(n,["header$"]);let i=P(".md-sidebar__scrollwrap",e),{y:a}=Ue(i);return C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=s.pipe(Le(0,me));return c.pipe(ee(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of $(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2})}}}),ue($("label[tabindex]",e)).pipe(oe(l=>d(l,"click").pipe(be(se),m(()=>l),U(p)))).subscribe(l=>{let f=P(`[id="${l.htmlFor}"]`);P(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),as(e,r).pipe(y(l=>s.next(l)),L(()=>s.complete()),m(l=>R({ref:e},l)))})}function fi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Ct(Ne(`${r}/releases/latest`).pipe(ve(()=>M),m(o=>({version:o.tag_name})),Be({})),Ne(r).pipe(ve(()=>M),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Be({}))).pipe(m(([o,n])=>R(R({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return Ne(r).pipe(m(o=>({repositories:o.public_repos})),Be({}))}}function ui(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Ne(r).pipe(ve(()=>M),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Be({}))}function di(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return fi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ui(r,o)}return M}var ss;function cs(e){return ss||(ss=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return M}return di(e.href).pipe(y(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>M),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),B(1)))}function hi(e){let t=P(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(Sn(o)),t.classList.add("md-source__repository--active")}),cs(e).pipe(y(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ps(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),Z("hidden"))}function bi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(G("navigation.tabs.sticky")?I({hidden:!1}):ps(e,t)).pipe(y(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ls(e,{viewport$:t,header$:r}){let o=new Map,n=$(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(Z("height"),m(({height:s})=>{let p=Se("main"),c=P(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(Z("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let h=f.offsetParent;for(;h;h=h.offsetParent)u+=h.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),We(i),v(([p,c])=>t.pipe(jr(([l,f],{offset:{y:u},size:h})=>{let w=u+h.height>=Math.floor(s.height);for(;f.length;){let[,A]=f[0];if(A-c=u&&!w)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Ye(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(X(),ne(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),G("toc.follow")){let s=S(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),We(o.pipe(be(se))),ee(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2,behavior:c})}}})}return G("navigation.tracking")&&t.pipe(U(a),Z("offset"),_e(250),Ce(1),U(n.pipe(Ce(1))),st({delay:250}),ee(i)).subscribe(([,{prev:s}])=>{let p=xe(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),ls(e,{viewport$:t,header$:r}).pipe(y(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function ms(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Ye(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),U(o.pipe(Ce(1))),ne(!0),st({delay:250}),m(a=>({hidden:a})))}function gi(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(U(a),Z("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),d(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),ms(e,{viewport$:t,main$:o,target$:n}).pipe(y(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))}function xi({document$:e,viewport$:t}){e.pipe(v(()=>$(".md-ellipsis")),oe(r=>tt(r).pipe(U(e.pipe(Ce(1))),b(o=>o),m(()=>r),we(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,lt(n,{viewport$:t}).pipe(U(e.pipe(Ce(1))),L(()=>n.removeAttribute("title")))})).subscribe(),e.pipe(v(()=>$(".md-status")),oe(r=>lt(r,{viewport$:t}))).subscribe()}function yi({document$:e,tablet$:t}){e.pipe(v(()=>$(".md-toggle--indeterminate")),y(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>d(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),ee(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function fs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ei({document$:e}){e.pipe(v(()=>$("[data-md-scrollfix]")),y(t=>t.removeAttribute("data-md-scrollfix")),b(fs),oe(t=>d(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function wi({viewport$:e,tablet$:t}){z([Ve("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),ee(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function us(){return location.protocol==="file:"?wt(`${new URL("search/search_index.js",Xr.base)}`).pipe(m(()=>__index),B(1)):Ne(new URL("search/search_index.json",Xr.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Yo(),jt=nn(),Ot=cn(jt),Zr=on(),Oe=bn(),hr=$t("(min-width: 960px)"),Si=$t("(min-width: 1220px)"),Oi=pn(),Xr=Te(),Mi=document.forms.namedItem("search")?us():Ke,eo=new g;Bn({alert$:eo});var to=new g;G("navigation.instant")&&Zn({location$:jt,viewport$:Oe,progress$:to}).subscribe(ot);var Ti;((Ti=Xr.version)==null?void 0:Ti.provider)==="mike"&&ii({document$:ot});S(jt,Ot).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});Zr.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&&pt(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&&pt(r);break;case"Enter":let o=Re();o instanceof HTMLLabelElement&&o.click()}});xi({viewport$:Oe,document$:ot});yi({document$:ot,tablet$:hr});Ei({document$:ot});wi({viewport$:Oe,tablet$:hr});var rt=Nn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Qn(e,{viewport$:Oe,header$:rt})),B(1)),ds=S(...ae("consent").map(e=>xn(e,{target$:Ot})),...ae("dialog").map(e=>Dn(e,{alert$:eo})),...ae("header").map(e=>zn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("palette").map(e=>Kn(e)),...ae("progress").map(e=>Yn(e,{progress$:to})),...ae("search").map(e=>li(e,{index$:Mi,keyboard$:Zr})),...ae("source").map(e=>hi(e))),hs=C(()=>S(...ae("announce").map(e=>gn(e)),...ae("content").map(e=>Un(e,{viewport$:Oe,target$:Ot,print$:Oi})),...ae("content").map(e=>G("search.highlight")?mi(e,{index$:Mi,location$:jt}):M),...ae("header-title").map(e=>qn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Si,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>bi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>vi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})),...ae("top").map(e=>gi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})))),Li=ot.pipe(v(()=>hs),Pe(ds),B(1));Li.subscribe();window.document$=ot;window.location$=jt;window.target$=Ot;window.keyboard$=Zr;window.viewport$=Oe;window.tablet$=hr;window.screen$=Si;window.print$=Oi;window.alert$=eo;window.progress$=to;window.component$=Li;})(); +//# sourceMappingURL=bundle.a7c05c9e.min.js.map + diff --git a/assets/javascripts/bundle.a7c05c9e.min.js.map b/assets/javascripts/bundle.a7c05c9e.min.js.map new file mode 100644 index 0000000..99cdca1 --- /dev/null +++ b/assets/javascripts/bundle.a7c05c9e.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/clipboard/dist/clipboard.js", "node_modules/escape-html/index.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/rxjs/node_modules/tslib/tslib.es6.js", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an