diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 37848a318..43ab47c9d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -23,7 +23,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v3.6.9 + placeholder: v3.7.2 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e6a5e76c2..2ad52023e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -7,6 +7,9 @@ contact_links: - name: ❓ Discussion url: https://github.com/netbox-community/netbox/discussions about: "If you're just looking for help, try starting a discussion instead." + - name: 🌎 Correct a Translation + url: https://explore.transifex.com/netbox-community/netbox/ + about: "Spot an incorrect translation? You can propose a fix on Transifex." - name: 💡 Plugin Idea url: https://plugin-ideas.netbox.dev about: "Have an idea for a plugin? Head over to the ideas board!" diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 006fb64fc..a198fd731 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.6.9 + placeholder: v3.7.2 validations: required: true - type: dropdown diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a52233034..af3d303b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ jobs: NETBOX_CONFIGURATION: netbox.configuration_testing strategy: matrix: - python-version: ['3.10', '3.11'] - node-version: ['14.x'] + python-version: ['3.10', '3.11', '3.12'] + node-version: ['18.x'] services: redis: image: redis @@ -68,6 +68,9 @@ jobs: - name: Collect static files run: python netbox/manage.py collectstatic --no-input + - name: Check for missing migrations + run: python netbox/manage.py makemigrations --check + - name: Check PEP8 compliance run: pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/ diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index a3e66a429..ad3bf5d75 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,13 +9,15 @@ on: permissions: issues: write pull-requests: write + discussions: write jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: issue-inactive-days: 90 pr-inactive-days: 30 + discussion-inactive-days: 180 issue-lock-reason: 'resolved' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 471846427..f94893021 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,12 +86,16 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli * In most cases, it is not necessary to add a changelog entry: A maintainer will take care of this when the PR is merged. (This helps avoid merge conflicts resulting from multiple PRs being submitted simultaneously.) -* All code submissions should meet the following criteria (CI will enforce these checks): +* All code submissions must meet the following criteria (CI will enforce these checks where feasible): + * Consist entirely of original work * Python syntax is valid * All tests pass when run with `./manage.py test` * PEP 8 compliance is enforced, with the exception that lines may be greater than 80 characters in length +> [!CAUTION] +> Any contributions which include AI-generated or reproduced content will be rejected. + * Some other tips to keep in mind: * If you'd like to volunteer for someone else's issue, please post a comment on that issue letting us know. (This will allow the maintainers to assign it to you.) * Check out our [developer docs](https://docs.netbox.dev/en/stable/development/getting-started/) for tips on setting up your development environment. @@ -117,8 +121,6 @@ We're always looking for motivated individuals to join the maintainers team and We generally ask that maintainers dedicate around four hours of work to the project each week on average, which includes both hands-on development and project management tasks such as issue triage. Maintainers are also encouraged (but not required) to attend our bi-weekly Zoom call to catch up on recent items. -Many maintainers petition their employer to grant some of their paid time to work on NetBox. In doing so, your employer becomes eligible to be featured as a [NetBox sponsor](https://github.com/netbox-community/netbox/wiki/Sponsorship). - Interested? You can contact our lead maintainer, Jeremy Stretch, at jeremy@netbox.dev or on the [NetDev Community Slack](https://netdev.chat/). We'd love to have you on the team! ## :heart: Other Ways to Contribute diff --git a/README.md b/README.md index 6e50e5687..f166919c4 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,129 @@
NetBox logo -

The premier source of truth powering network automation

- CI status +

The cornerstone of every automated network

+ Latest release + License + Contributors + GitHub stars + Languages supported + CI status

-NetBox is the leading solution for modeling and documenting modern networks. By -combining the traditional disciplines of IP address management (IPAM) and -datacenter infrastructure management (DCIM) with powerful APIs and extensions, -NetBox provides the ideal "source of truth" to power network automation. -Available as open source software under the Apache 2.0 license, NetBox serves -as the cornerstone for network automation in thousands of organizations. +NetBox exists to empower network engineers. Since its release in 2016, it has become the go-to solution for modeling and documenting network infrastructure for thousands of organizations worldwide. As a successor to legacy IPAM and DCIM applications, NetBox provides a cohesive, extensive, and accessible data model for all things networked. By providing a single robust user interface and programmable APIs for everything from cable maps to device configurations, NetBox serves as the central source of truth for the modern network. -* **Physical infrastructure:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power! -* **Modern IPAM:** All the standard IPAM functionality you expect, plus VRF import/export tracking, VLAN management, and overlay support. -* **Data circuits:** Confidently manage the delivery of critical circuits from various service providers, modeled seamlessly alongside your own infrastructure. -* **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets. -* **Organization:** Manage tenant and contact assignments natively. -* **Powerful search:** Easily find anything you need using a single global search function. -* **Comprehensive logging:** Leverage both automatic change logging and user-submitted journal entries to track your network's growth over time. -* **Endless customization:** Custom fields, custom links, tags, export templates, custom validation, reports, scripts, and more! -* **Flexible permissions:** An advanced permissions systems enables very flexible delegation of permissions. -* **Integrations:** Easily connect NetBox to your other tooling via its REST & GraphQL APIs. -* **Plugins:** Not finding what you need in the core application? Try one of many community plugins - or build your own! +

+ NetBox's Role | + Why NetBox? | + Getting Started | + Get Involved | + Project Stats | + Screenshots +

-![Screenshot of NetBox UI](docs/media/screenshots/netbox-ui.png "NetBox UI") +

+ NetBox user interface screenshot +

+ +## NetBox's Role + +NetBox functions as the **source of truth** for your network infrastructure. Its job is to define and validate the _intended state_ of all network components and resources. NetBox does not interact with network nodes directly; rather, it makes this data available programmatically to purpose-built automation, monitoring, and assurance tools. This separation of duties enables the construction of a robust yet flexible automation system. + +

+ Reference network automation architecture +

+ +The diagram above illustrates the recommended deployment architecture for an automated network, leveraging NetBox as the central authority for network state. This approach allows your team to swap out individual tools to meet changing needs while retaining a predictable, modular workflow. + +## Why NetBox? + +### Comprehensive Data Model + +Racks, devices, cables, IP addresses, VLANs, circuits, power, VPNs, and lots more: NetBox is built for networks. Its comprehensive and thoroughly inter-linked data model provides for natural and highly structured modeling of myriad network primitives that just isn't possible using general-purpose tools. And there's no need to waste time contemplating how to build out a database: Everything is ready to go upon installation. + +### Focused Development + +NetBox strives to meet a singular goal: Provide the best available solution for making network infrastructure programmatically accessible. Unlike "all-in-one" tools which awkwardly bolt on half-baked features in an attempt to check every box, NetBox is committed to its core function. NetBox provides the best possible solution for modeling network infrastructure, and provides rich APIs for integrating with tools that excel in other areas of network automation. + +### Extensible and Customizable + +No two networks are exactly the same. Users are empowered to extend NetBox's native data model with custom fields and tags to best suit their unique needs. You can even write your own plugins to introduce entirely new objects and functionality! + +### Flexible Permissions + +NetBox includes a fully customizable permission system, which affords administrators incredible granularity when assigning roles to users and groups. Want to restrict certain users to working only with cabling and not be able to change IP addresses? Or maybe each team should have access only to a particular tenant? NetBox enables you to craft roles as you see fit. + +### Custom Validation & Protection Rules + +The data you put into NetBox is crucial to network operations. In addition to its robust native validation rules, NetBox provides mechanisms for administrators to define their own custom validation rules for objects. Custom validation can be used both to ensure new or modified objects adhere to a set of rules, and to prevent the deletion of objects which don't meet certain criteria. (For example, you might want to prevent the deletion of a device with an "active" status.) + +### Device Configuration Rendering + +NetBox can render user-created Jinja2 templates to generate device configurations from its own data. Configuration templates can be uploaded individually or pulled automatically from an external source, such as a git repository. Rendered configurations can be retrieved via the REST API for application directly to network devices via a provisioning tool such as Ansible or Salt. + +### Custom Scripts + +Complex workflows, such as provisioning a new branch office, can be tedious to carry out via the user interface. NetBox allows you to write and upload custom scripts that can be run directly from the UI. Scripts prompt users for input and then automate the necessary tasks to greatly simplify otherwise burdensome processes. + +### Automated Events + +Users can define event rules to automatically trigger a custom script or outbound webhook in response to a NetBox event. For example, you might want to automatically update a network monitoring service whenever a new device is added to NetBox, or update a DHCP server when an IP range is allocated. + +### Comprehensive Change Logging + +NetBox automatically logs the creation, modification, and deletion of all managed objects, providing a thorough change history. Changes can be attributed to the executing user, and related changes are grouped automatically by request ID. + +> [!NOTE] +> A complete list of NetBox's myriad features can be found in [the introductory documentation](https://docs.netbox.dev/en/stable/introduction/). ## Getting Started -
- - [![NetBox logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy1.png)](https://github.com/netbox-community/netbox) -            - [![Docker logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy2.png)](https://github.com/netbox-community/netbox-docker) -            - [![NetBox Labs logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy3.png)](https://netboxlabs.com/netbox-cloud/) - -
- * Just want to explore? Check out [our public demo](https://demo.netbox.dev/) right now! * The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction. * Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox! +

+ NetBox Cloud
+ Looking for an enterprise solution? Check out NetBox Cloud! +

+ ## Get Involved * Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter! * Join the conversation on [the discussion forum](https://github.com/netbox-community/netbox/discussions) and [Slack](https://netdev.chat/)! * Already a power user? You can [suggest a feature](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+feature&template=feature_request.yaml) or [report a bug](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+bug&template=bug_report.yaml) on GitHub. * Contributions from the community are encouraged and appreciated! Check out our [contributing guide](CONTRIBUTING.md) to get started. +* [Share your idea](https://plugin-ideas.netbox.dev/) for a new plugin, or [learn how to build one](https://github.com/netbox-community/netbox-plugin-tutorial) yourself! ## Project Stats -
+

Timeline graph Issues graph Pull requests graph Top contributors
Stats via Repography -

- -## Sponsors - -
- - [![NetBox Labs](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/netbox_labs.png)](https://netboxlabs.com) -            - [![DigitalOcean](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/digitalocean.png)](https://try.digitalocean.com/developer-cloud) -            - [![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io) -
- [![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com) -            - [![OneMind Services](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/onemind_services.png)](https://onemindservices.com) - -
+

## Screenshots -![Screenshot of main page (dark mode)](docs/media/screenshots/home-dark.png "Main page (dark mode)") - -![Screenshot of rack elevation](docs/media/screenshots/rack.png "Rack elevation") - -![Screenshot of prefixes hierarchy](docs/media/screenshots/prefixes-list.png "Prefixes hierarchy") - -![Screenshot of cable trace](docs/media/screenshots/cable-trace.png "Cable tracing") +

+ NetBox Dashboard (Light Mode)
+ NetBox dashboard (light mode) +

+

+ NetBox Dashboard (Dark Mode)
+ NetBox dashboard (dark mode) +

+

+ Prefixes List
+ Prefixes list +

+

+ Rack View
+ Rack view +

+

+ Cable Trace
+ Cable trace +

diff --git a/base_requirements.txt b/base_requirements.txt index 715f7ad74..580c5f6ad 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -1,10 +1,6 @@ -# HTML sanitizer -# https://github.com/mozilla/bleach/blob/main/CHANGES -bleach - # The Python web framework on which NetBox is built # https://docs.djangoproject.com/en/stable/releases/ -Django<5.0 +Django<5.1 # Django middleware which permits cross-domain API requests # https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst @@ -22,6 +18,10 @@ django-filter # https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst django-graphiql-debug-toolbar +# HTMX utilities for Django +# https://django-htmx.readthedocs.io/en/latest/changelog.html +django-htmx + # Modified Preorder Tree Traversal (recursive nesting of objects) # Pinned to 0.14.0; 0.15.0 requires Python 3.9+ # https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst @@ -108,6 +108,10 @@ mkdocstrings[python-legacy] # https://github.com/netaddr/netaddr/blob/master/CHANGELOG netaddr +# Python bindings to the ammonia HTML sanitization library. +# https://github.com/messense/nh3 +nh3 + # Fork of PIL (Python Imaging Library) for image processing # https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst Pillow diff --git a/docs/administration/authentication/microsoft-azure-ad.md b/docs/administration/authentication/microsoft-azure-ad.md index ee24e8232..17b130818 100644 --- a/docs/administration/authentication/microsoft-azure-ad.md +++ b/docs/administration/authentication/microsoft-azure-ad.md @@ -73,7 +73,7 @@ You should be redirected to Microsoft's authentication portal. Enter the usernam If successful, you will be redirected back to the NetBox UI, and will be logged in as the AD user. You can verify this by navigating to your profile (using the button at top right). -This user account has been replicated locally to NetBox, and can now be assigned groups and permissions within the NetBox admin UI. +This user account has been replicated locally to NetBox, and can now be assigned groups and permissions. ## Troubleshooting diff --git a/docs/administration/authentication/okta.md b/docs/administration/authentication/okta.md index ff552d730..2cab186ee 100644 --- a/docs/administration/authentication/okta.md +++ b/docs/administration/authentication/okta.md @@ -67,4 +67,4 @@ You should be redirected to Okta's authentication portal. Enter the username/ema If successful, you will be redirected back to the NetBox UI, and will be logged in as the Okta user. You can verify this by navigating to your profile (using the button at top right). -This user account has been replicated locally to NetBox, and can now be assigned groups and permissions within the NetBox admin UI. +This user account has been replicated locally to NetBox, and can now be assigned groups and permissions. diff --git a/docs/administration/authentication/overview.md b/docs/administration/authentication/overview.md index 8a8b8f60b..3a3b9efc2 100644 --- a/docs/administration/authentication/overview.md +++ b/docs/administration/authentication/overview.md @@ -2,9 +2,9 @@ ## Local Authentication -Local user accounts and groups can be created in NetBox under the "Authentication and Authorization" section of the administrative user interface. This interface is available only to users with the "staff" permission enabled. +Local user accounts and groups can be created in NetBox under the "Authentication" section in the "Admin" menu. This section is available only to users with the "staff" permission enabled. -At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to users and/or groups within the admin UI. +At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to individual users and/or groups as needed. ## Remote Authentication diff --git a/docs/configuration/date-time.md b/docs/configuration/date-time.md index ab8b5ad13..a23053e08 100644 --- a/docs/configuration/date-time.md +++ b/docs/configuration/date-time.md @@ -10,6 +10,9 @@ The time zone NetBox will use when dealing with dates and times. It is recommend You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date). Default formats are listed below. +!!! note + These system defaults will be overridden by a user's selected language/locale when [localization](./system.md#enable_localization) is enabled. + ```python DATE_FORMAT = 'N j, Y' # June 26, 2016 SHORT_DATE_FORMAT = 'Y-m-d' # 2016-06-26 diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 70466d029..6a2ecdc7f 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -46,4 +46,4 @@ The configuration file may be modified at any time. However, the WSGI service (e $ sudo systemctl restart netbox ``` -Configuration parameters which are set via the admin UI (those listed under "dynamic settings") take effect immediately. +Dynamic configuration parameters (those which can be modified via the UI) take effect immediately. diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md index 4d4ca189e..7e68bcee7 100644 --- a/docs/configuration/miscellaneous.md +++ b/docs/configuration/miscellaneous.md @@ -99,6 +99,14 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da --- +## DJANGO_ADMIN_ENABLED + +Default: False + +Setting this to True installs the `django.contrib.admin` app and enables the [Django admin UI](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/). This may be necessary to support older plugins which do not integrate with the native NetBox interface. + +--- + ## ENFORCE_GLOBAL_UNIQUE !!! tip "Dynamic Configuration Parameter" diff --git a/docs/configuration/system.md b/docs/configuration/system.md index 7fbf9ec54..806839778 100644 --- a/docs/configuration/system.md +++ b/docs/configuration/system.md @@ -69,15 +69,7 @@ Email is sent from NetBox only for critical events or if configured for [logging Default: False -Determines if localization features are enabled or not. This should only be enabled for development or testing purposes as netbox is not yet fully localized. Turning this on will localize numeric and date formats (overriding what is set for DATE_FORMAT) based on the browser locale as well as translate certain strings from third party modules. - ---- - -## GIT_PATH - -Default: `git` - -The system path to the `git` executable, used by the synchronization backend for remote git repositories. +Determines if localization features are enabled or not. This should only be enabled for development or testing purposes as netbox is not yet fully localized. Turning this on will localize numeric and date formats (overriding any configured [system defaults](./date-time.md#date-and-time-formatting)) based on the browser locale as well as translate certain strings from third party modules. --- diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 0b1ed11df..e2bc53cfc 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -288,9 +288,9 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a ## Running Custom Scripts !!! note - To run a custom script, a user must be assigned via permissions for `Extras > Script`, `Extras > ScriptModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below. + To run a custom script, a user must be assigned permissions for `Extras > Script`, `Extras > Script Module`, and `Core > Managed File` objects. They must also be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in "Permissions" as shown below. - ![Adding the run action to a permission](../media/admin_ui_run_permission.png) + ![Adding the run action to a permission](../media/run_permission.png) ### Via the Web UI diff --git a/docs/customization/reports.md b/docs/customization/reports.md index a821c5da7..8b0fc44f3 100644 --- a/docs/customization/reports.md +++ b/docs/customization/reports.md @@ -132,9 +132,9 @@ Once you have created a report, it will appear in the reports list. Initially, r ## Running Reports !!! note - To run a report, a user must be assigned via permissions for `Extras > Report`, `Extras > ReportModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below. + To run a report, a user must be assigned permissions for `Extras > Report`, `Extras > Report Module`, and `Core > Managed File` objects. They must also be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in "Permissions" as shown below. - ![Adding the run action to a permission](../media/admin_ui_run_permission.png) + ![Adding the run action to a permission](../media/run_permission.png) ### Via the Web UI diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 68b777111..2af640546 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -80,6 +80,18 @@ Run the following command to update the device type definition validation schema This will automatically update the schema file at `contrib/generated_schema.json`. +### Update & Compile Translations + +Log into [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) to download the updated string maps. Download the resource (portable object, or `.po`) file for each language and save them to `netbox/translations/$lang/LC_MESSAGES/django.po`, overwriting the current files. (Be sure to click the **Download for use** link.) + +![Transifex download](../media/development/transifex_download.png) + +Once the resource files for all languages have been updated, compile the machine object (`.mo`) files using the `compilemessages` management command: + +```nohighlight +./manage.py compilemessages +``` + ### Update Version and Changelog * Update the `VERSION` constant in `settings.py` to the new release version. @@ -90,7 +102,7 @@ Commit these changes to the `develop` branch and push upstream. ### Verify CI Build Status -Ensure that continuous integration testing on the `develop` branch is completing successfully. If it fails, take action to correct the failure before proceding with the release. +Ensure that continuous integration testing on the `develop` branch is completing successfully. If it fails, take action to correct the failure before proceeding with the release. ### Submit a Pull Request diff --git a/docs/development/translations.md b/docs/development/translations.md new file mode 100644 index 000000000..e40f996c5 --- /dev/null +++ b/docs/development/translations.md @@ -0,0 +1,30 @@ +# Translations + +NetBox coordinates all translation work using the [Transifex](https://explore.transifex.com/netbox-community/netbox/) platform. Signing up for a Transifex account is free. + +All language translations in NetBox are generated from the source file found at `netbox/translations/en/LC_MESSAGES/django.po`. This file contains the original English strings with empty mappings, and is generated as part of NetBox's release process. Transifex updates source strings from this file on a recurring basis, so new translation strings will appear in the platform automatically as it is updated in the code base. + +Reviewers log into Transifex and navigate to their designated language(s) to translate strings. The initial translation for most strings will be machine-generated via the AWS Translate service. Human reviewers are responsible for reviewing these translations and making corrections where necessary. + +Immediately prior to each NetBox release, the translation maps for all completed languages will be downloaded from Transifex, compiled, and checked into the NetBox code base by a maintainer. + +## Updating Translation Sources + +To update the English `.po` file from which all translations are derived, use the `makemessages` management command: + +```nohighlight +./manage.py makemessages -l en +``` + +Then, commit the change and push to the `develop` branch on GitHub. After some time, any new strings will appear for translation on Transifex automatically. + +## Proposing New Languages + +If you'd like to add support for a new language to NetBox, the first step is to [submit a GitHub issue](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+translation&projects=&template=translation.yaml) to capture the proposal. While we'd like to add as many languages as possible, we do need to limit the rate at which new languages are added. New languages will be selected according to community interest and the number of volunteers who sign up as translators. + +Once a proposed language has been approved, a NetBox maintainer will: + +* Add it to the Transifex platform +* Designate one or more reviewers +* Create the initial machine-generated translations for review +* Add it to the list of supported languages diff --git a/docs/features/configuration-rendering.md b/docs/features/configuration-rendering.md index a87a6eae4..44cacc684 100644 --- a/docs/features/configuration-rendering.md +++ b/docs/features/configuration-rendering.md @@ -39,7 +39,7 @@ When rendered for a specific NetBox device, the template's `device` variable wil ### Context Data -The objet for which the configuration is being rendered is made available as template context as `device` or `virtualmachine` for devices and virtual machines, respectively. Additionally, NetBox model classes can be accessed by the app or plugin in which they reside. For example: +The object for which the configuration is being rendered is made available as template context as `device` or `virtualmachine` for devices and virtual machines, respectively. Additionally, NetBox model classes can be accessed by the app or plugin in which they reside. For example: ``` There are {{ dcim.Site.objects.count() }} sites. @@ -70,6 +70,11 @@ This request will trigger resolution of the device's preferred config template i If no config template has been assigned to any of these three objects, the request will fail. +The configuration can be rendered as JSON or as plaintext by setting the `Accept:` HTTP header. For example: + +* `Accept: application/json` +* `Accept: text/plain` + ### General Purpose Use NetBox config templates can also be rendered without being tied to any specific device, using a separate general purpose REST API endpoint. Any data included with a POST request to this endpoint will be passed as context data for the template. diff --git a/docs/features/event-rules.md b/docs/features/event-rules.md index 0e9535223..158dc111a 100644 --- a/docs/features/event-rules.md +++ b/docs/features/event-rules.md @@ -28,4 +28,4 @@ For more detail, see the reference documentation for NetBox's [conditional logic ## Event Rule Processing -When a change is detected, any resulting events are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing event(s) to be processed. The events are then extracted from the queue by the `rqworker` process. The current event queue and any failed events can be inspected in the admin UI under System > Background Tasks. +When a change is detected, any resulting events are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing event(s) to be processed. The events are then extracted from the queue by the `rqworker` process. The current event queue and any failed events can be inspected under System > Background Tasks. diff --git a/docs/features/synchronized-data.md b/docs/features/synchronized-data.md index a070d0ce1..8c95c8779 100644 --- a/docs/features/synchronized-data.md +++ b/docs/features/synchronized-data.md @@ -1,6 +1,6 @@ # Synchronized Data -Several models in NetBox support the automatic synchronization of local data from a designated remote source. For example, [configuration templates](./configuration-rendering.md) defined in NetBox can source their content from text files stored in a remote git repository. This accomplished using the core [data source](../models/core/datasource.md) and [data file](../models/core/datafile.md) models. +Several models in NetBox support the automatic synchronization of local data from a designated remote source. For example, [configuration templates](./configuration-rendering.md) defined in NetBox can source their content from text files stored in a remote git repository. This is accomplished using the core [data source](../models/core/datasource.md) and [data file](../models/core/datafile.md) models. To enable remote data synchronization, the NetBox administrator first designates one or more remote data sources. NetBox currently supports the following source types: diff --git a/docs/index.md b/docs/index.md index 84334337b..5ef650ca6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ NetBox is the leading solution for modeling and documenting modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. Read on to discover why thousands of organizations worldwide put NetBox at the heart of their infrastructure. -[![NetBox UI](./media/screenshots/netbox-ui.png)](./media/screenshots/netbox-ui.png) +[![NetBox UI](./media/screenshots/home-light.png)](./media/screenshots/home-light.png) ## :material-server-network: Built for Networks diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index 80d787254..9a143319d 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -7,7 +7,7 @@ This section of the documentation discusses installing and configuring the NetBo Begin by installing all system packages required by NetBox and its dependencies. !!! warning "Python 3.10 or later required" - NetBox requires Python 3.10 or 3.11. + NetBox supports Python 3.10, 3.11, and 3.12. === "Ubuntu" diff --git a/docs/installation/4-gunicorn.md b/docs/installation/4-gunicorn.md index 1183a9123..e31c48466 100644 --- a/docs/installation/4-gunicorn.md +++ b/docs/installation/4-gunicorn.md @@ -58,3 +58,6 @@ You should see output similar to the following: If the NetBox service fails to start, issue the command `journalctl -eu netbox` to check for log messages that may indicate the problem. Once you've verified that the WSGI workers are up and running, move on to HTTP server setup. + +!!! note + There is a bug in the current stable release of gunicorn (v21.2.0) where automatic restarts of the worker processes can result in 502 errors under heavy load. (See [gunicorn bug #3038](https://github.com/benoitc/gunicorn/issues/3038) for more detail.) Users who encounter this issue may opt to downgrade to an earlier, unaffected release of gunicorn (`pip install gunicorn==20.1.0`). Note, however, that this earlier release does not officially support Python 3.11. diff --git a/docs/installation/index.md b/docs/installation/index.md index 4022bf1ad..6ad4aa79e 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -18,11 +18,11 @@ The following sections detail how to set up a new instance of NetBox: ## Requirements -| Dependency | Minimum Version | -|------------|-----------------| -| Python | 3.10 | -| PostgreSQL | 12 | -| Redis | 4.0 | +| Dependency | Supported Versions | +|------------|--------------------| +| Python | 3.10, 3.11, 3.12 | +| PostgreSQL | 12+ | +| Redis | 4.0+ | Below is a simplified overview of the NetBox application stack for reference: diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 4e678c013..6093b226e 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -17,11 +17,11 @@ Prior to upgrading your NetBox instance, be sure to carefully review all [releas NetBox requires the following dependencies: -| Dependency | Minimum Version | -|------------|-----------------| -| Python | 3.10 | -| PostgreSQL | 12 | -| Redis | 4.0 | +| Dependency | Supported Versions | +|------------|--------------------| +| Python | 3.10, 3.11, 3.12 | +| PostgreSQL | 12+ | +| Redis | 4.0+ | ## 3. Install the Latest Release diff --git a/docs/integrations/webhooks.md b/docs/integrations/webhooks.md index 8913fd99c..bfe9ddb2d 100644 --- a/docs/integrations/webhooks.md +++ b/docs/integrations/webhooks.md @@ -73,9 +73,9 @@ If no body template is specified, the request body will be populated with a JSON ## Webhook Processing -Using [Event Rules](../features/event-rules.md), when a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks. +Using [Event Rules](../features/event-rules.md), when a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected under System > Background Tasks. -A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI. +A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be requeued manually under System > Background Tasks. ## Troubleshooting @@ -106,6 +106,6 @@ Content-Type: application/x-www-form-urlencoded ------------ ``` -Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection. +Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection. If you don't see any output, check that the `rqworker` process is running and that webhook events are being placed into the queue. -Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI). +Webhook results can be found in the NetBox admin UI under the Background Tasks section. You can see any finished or failed runs, as well as the error log for failed webhooks. diff --git a/docs/media/development/transifex_download.png b/docs/media/development/transifex_download.png new file mode 100644 index 000000000..99429ce11 Binary files /dev/null and b/docs/media/development/transifex_download.png differ diff --git a/docs/media/misc/netbox_cloud.png b/docs/media/misc/netbox_cloud.png new file mode 100644 index 000000000..f9deca674 Binary files /dev/null and b/docs/media/misc/netbox_cloud.png differ diff --git a/docs/media/misc/reference_architecture.png b/docs/media/misc/reference_architecture.png new file mode 100644 index 000000000..89ed4478d Binary files /dev/null and b/docs/media/misc/reference_architecture.png differ diff --git a/docs/media/admin_ui_run_permission.png b/docs/media/run_permission.png similarity index 100% rename from docs/media/admin_ui_run_permission.png rename to docs/media/run_permission.png diff --git a/docs/media/screenshots/cable-trace.png b/docs/media/screenshots/cable-trace.png index b35272016..e228d1786 100644 Binary files a/docs/media/screenshots/cable-trace.png and b/docs/media/screenshots/cable-trace.png differ diff --git a/docs/media/screenshots/home-dark.png b/docs/media/screenshots/home-dark.png index 718413445..7b060785f 100644 Binary files a/docs/media/screenshots/home-dark.png and b/docs/media/screenshots/home-dark.png differ diff --git a/docs/media/screenshots/home-light.png b/docs/media/screenshots/home-light.png new file mode 100644 index 000000000..1eaca3ef0 Binary files /dev/null and b/docs/media/screenshots/home-light.png differ diff --git a/docs/media/screenshots/netbox-ui.png b/docs/media/screenshots/netbox-ui.png deleted file mode 100644 index 70cd77089..000000000 Binary files a/docs/media/screenshots/netbox-ui.png and /dev/null differ diff --git a/docs/media/screenshots/prefixes-list.png b/docs/media/screenshots/prefixes-list.png index 927a7a04e..7220a8817 100644 Binary files a/docs/media/screenshots/prefixes-list.png and b/docs/media/screenshots/prefixes-list.png differ diff --git a/docs/media/screenshots/rack.png b/docs/media/screenshots/rack.png index dbe9718f7..7179efda3 100644 Binary files a/docs/media/screenshots/rack.png and b/docs/media/screenshots/rack.png differ diff --git a/docs/models/vpn/ikepolicy.md b/docs/models/vpn/ikepolicy.md index 7b739072b..d2da28d16 100644 --- a/docs/models/vpn/ikepolicy.md +++ b/docs/models/vpn/ikepolicy.md @@ -14,7 +14,7 @@ The IKE version employed (v1 or v2). ### Mode -The IKE mode employed (main or aggressive). +The mode employed (main or aggressive) when IKEv1 is in use. This setting is not supported for IKEv2. ### Proposals diff --git a/docs/plugins/development/dashboard-widgets.md b/docs/plugins/development/dashboard-widgets.md index b1c9d0e45..74f9c9474 100644 --- a/docs/plugins/development/dashboard-widgets.md +++ b/docs/plugins/development/dashboard-widgets.md @@ -47,3 +47,14 @@ class ReminderWidget(DashboardWidget): def render(self, request): return self.config.get('content') ``` + +## Initialization + +To register the widget, it becomes essential to import the widget module. The recommended approach is to accomplish this within the `ready` method situated in your `PluginConfig`: + +```python +class FooBarConfig(PluginConfig): + def ready(self): + super().ready() + from . import widgets # point this to the above widget module you created +``` diff --git a/docs/plugins/development/data-backends.md b/docs/plugins/development/data-backends.md index feffa5bed..8b7226a41 100644 --- a/docs/plugins/development/data-backends.md +++ b/docs/plugins/development/data-backends.md @@ -20,4 +20,4 @@ backends = [MyDataBackend] !!! tip The path to the list of search indexes can be modified by setting `data_backends` in the PluginConfig instance. -::: core.data_backends.DataBackend +::: netbox.data_backends.DataBackend diff --git a/docs/release-notes/version-3.7.md b/docs/release-notes/version-3.7.md index d6818bdec..103b0664c 100644 --- a/docs/release-notes/version-3.7.md +++ b/docs/release-notes/version-3.7.md @@ -1,5 +1,60 @@ # NetBox v3.7 +## v3.7.3 (FUTURE) + +--- + +## v3.7.2 (2024-02-05) + +### Enhancements + +* [#13729](https://github.com/netbox-community/netbox/issues/13729) - Omit sensitive data source parameters from change log data +* [#14645](https://github.com/netbox-community/netbox/issues/14645) - Limit the number of assigned IP addresses displayed under interfaces list + +### Bug Fixes + +* [#14500](https://github.com/netbox-community/netbox/issues/14500) - Optimize calculation of available child prefixes & ranges when viewing a prefix +* [#14511](https://github.com/netbox-community/netbox/issues/14511) - Fix GraphQL support for interfaces connected to provider networks +* [#14572](https://github.com/netbox-community/netbox/issues/14572) - Correct the number of jobs listed for individual report & script modules +* [#14703](https://github.com/netbox-community/netbox/issues/14703) - Revert to the default layout when encountering a misconfigured dashboard +* [#14755](https://github.com/netbox-community/netbox/issues/14755) - Fix validation of choice values & labels when creating a custom field choice set via the REST API +* [#14838](https://github.com/netbox-community/netbox/issues/14838) - Avoid corrupting JSON data when changing the action type while editing an event rule +* [#14839](https://github.com/netbox-community/netbox/issues/14839) - Fix form validation error when attempting to terminate a tunnel to a virtual machine interface +* [#14840](https://github.com/netbox-community/netbox/issues/14840) - Fix `NoReverseMatch` exception when rendering a custom field which references a user +* [#14847](https://github.com/netbox-community/netbox/issues/14847) - IKE policy mode may be set inly when IKEv1 is selected +* [#14851](https://github.com/netbox-community/netbox/issues/14851) - Automatically remove any associated bookmarks when deleting a user +* [#14879](https://github.com/netbox-community/netbox/issues/14879) - Include custom fields in REST API representation of data sources +* [#14885](https://github.com/netbox-community/netbox/issues/14885) - Add missing "group" field to VPN tunnel creation form +* [#14892](https://github.com/netbox-community/netbox/issues/14892) - Fix exception when running report/script via command line due to missing username +* [#14920](https://github.com/netbox-community/netbox/issues/14920) - Include button to display available status choices when bulk importing virtual device contexts +* [#14945](https://github.com/netbox-community/netbox/issues/14945) - Fix "select all" button for device type components +* [#14947](https://github.com/netbox-community/netbox/issues/14947) - Ensure that application & removal of tags is always recorded in an object's change log +* [#14962](https://github.com/netbox-community/netbox/issues/14962) - Fix config context rendering for VMs assigned directly to a site (rather than via a cluster) +* [#14999](https://github.com/netbox-community/netbox/issues/14999) - Fix "create & add another" link for interface FHRP group assignment +* [#15015](https://github.com/netbox-community/netbox/issues/15015) - Pre-populate assigned tenant when allocating next available IP address under prefix view +* [#15020](https://github.com/netbox-community/netbox/issues/15020) - Automatically update all VMs when changing a cluster's assigned site +* [#15025](https://github.com/netbox-community/netbox/issues/15025) - The `can_add()` template filter should accept a model (not an instance) + +--- + +## v3.7.1 (2024-01-17) + +### Bug Fixes + +* [#13844](https://github.com/netbox-community/netbox/issues/13844) - Use `available_at_site` filter when filtering VLANs under prefix form +* [#14663](https://github.com/netbox-community/netbox/issues/14663) - Fix tunnel creation when setting initial termination to a VM interface +* [#14706](https://github.com/netbox-community/netbox/issues/14706) - Relax one-to-one mapping of tunnel termination to IP address +* [#14709](https://github.com/netbox-community/netbox/issues/14709) - Fix typo in tunnel termination type choice name +* [#14749](https://github.com/netbox-community/netbox/issues/14749) - Remove errant translation wrapper from `installed_device` on DeviceBay +* [#14778](https://github.com/netbox-community/netbox/issues/14778) - Custom field API serializer should accept null values for all optional fields +* [#14791](https://github.com/netbox-community/netbox/issues/14791) - Hide available prefixes when searching within a parent prefix +* [#14793](https://github.com/netbox-community/netbox/issues/14793) - Add missing Diffie-Hellman group 15 +* [#14816](https://github.com/netbox-community/netbox/issues/14816) - Ensure default contact assignment ordering is consistent +* [#14817](https://github.com/netbox-community/netbox/issues/14817) - Relax required fields for IKE & IPSec models on bulk import +* [#14827](https://github.com/netbox-community/netbox/issues/14827) - Ensure all matching event rules are processed in response to an event + +--- + ## v3.7.0 (2023-12-29) ### Breaking Changes diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md new file mode 100644 index 000000000..e2502df8c --- /dev/null +++ b/docs/release-notes/version-4.0.md @@ -0,0 +1,28 @@ +# NetBox v4.0 + +## v4.0.0 (FUTURE) + +### New Features + +#### Complete UI Refresh ([#12128](https://github.com/netbox-community/netbox/issues/12128)) + +The NetBox user interface has been completely refreshed and updated. + +### Enhancements + +* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3 +* [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0 +* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12 +* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI +* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI + +### Other Changes + +* [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it) +* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses a custom User model rather than the stock model provided by Django +* [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7 +* [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`) +* [#14638](https://github.com/netbox-community/netbox/issues/14638) - Drop support for Python 3.8 and 3.9 +* [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin` +* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`) +* [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class diff --git a/mkdocs.yml b/mkdocs.yml index 5a7e00c2c..e1128578a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -286,6 +286,7 @@ nav: - User Preferences: 'development/user-preferences.md' - Web UI: 'development/web-ui.md' - Internationalization: 'development/internationalization.md' + - Translations: 'development/translations.md' - Release Checklist: 'development/release-checklist.md' - git Cheat Sheet: 'development/git-cheat-sheet.md' - Release Notes: diff --git a/netbox/account/views.py b/netbox/account/views.py index 3dbba9b29..40ce78039 100644 --- a/netbox/account/views.py +++ b/netbox/account/views.py @@ -2,8 +2,8 @@ import logging from django.conf import settings from django.contrib import messages -from django.contrib.auth import login as auth_login, logout as auth_logout -from django.contrib.auth import update_session_auth_hash +from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash +from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import update_last_login from django.contrib.auth.signals import user_logged_in @@ -72,7 +72,7 @@ class LoginView(View): return auth_backends def get(self, request): - form = forms.LoginForm(request) + form = AuthenticationForm(request) if request.user.is_authenticated: logger = logging.getLogger('netbox.auth.login') @@ -85,7 +85,7 @@ class LoginView(View): def post(self, request): logger = logging.getLogger('netbox.auth.login') - form = forms.LoginForm(request, data=request.POST) + form = AuthenticationForm(request, data=request.POST) if form.is_valid(): logger.debug("Login form validation was successful") @@ -220,7 +220,7 @@ class ChangePasswordView(LoginRequiredMixin, View): messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.") return redirect('account:profile') - form = forms.PasswordChangeForm(user=request.user) + form = PasswordChangeForm(user=request.user) return render(request, self.template_name, { 'form': form, @@ -228,7 +228,7 @@ class ChangePasswordView(LoginRequiredMixin, View): }) def post(self, request): - form = forms.PasswordChangeForm(user=request.user, data=request.POST) + form = PasswordChangeForm(user=request.user, data=request.POST) if form.is_valid(): form.save() update_session_auth_hash(request, form.user) diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index 0c30e3cda..8127d5bcb 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -7,7 +7,6 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant -from utilities.forms import BootstrapMixin from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField __all__ = ( @@ -112,7 +111,7 @@ class CircuitImportForm(NetBoxModelImportForm): ] -class CircuitTerminationImportForm(BootstrapMixin, forms.ModelForm): +class CircuitTerminationImportForm(forms.ModelForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), diff --git a/netbox/circuits/migrations/0003_extend_tag_support.py b/netbox/circuits/migrations/0003_extend_tag_support.py deleted file mode 100644 index e5e6ee262..000000000 --- a/netbox/circuits/migrations/0003_extend_tag_support.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-21 14:50 - -from django.db import migrations -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0062_clear_secrets_changelog'), - ('circuits', '0002_squashed_0029'), - ] - - operations = [ - migrations.AddField( - model_name='circuittype', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - ] diff --git a/netbox/circuits/migrations/0003_squashed_0037.py b/netbox/circuits/migrations/0003_squashed_0037.py new file mode 100644 index 000000000..69c3e1c68 --- /dev/null +++ b/netbox/circuits/migrations/0003_squashed_0037.py @@ -0,0 +1,127 @@ +import taggit.managers +from django.db import migrations, models + +import utilities.json + + +class Migration(migrations.Migration): + + replaces = [ + ('circuits', '0003_extend_tag_support'), + ('circuits', '0004_rename_cable_peer'), + ('circuits', '0032_provider_service_id'), + ('circuits', '0033_standardize_id_fields'), + ('circuits', '0034_created_datetimefield'), + ('circuits', '0035_provider_asns'), + ('circuits', '0036_circuit_termination_date_tags_custom_fields'), + ('circuits', '0037_new_cabling_models') + ] + + dependencies = [ + ('ipam', '0047_squashed_0053'), + ('extras', '0002_squashed_0059'), + ('circuits', '0002_squashed_0029'), + ] + + operations = [ + migrations.AddField( + model_name='circuittype', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.RenameField( + model_name='circuittermination', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='circuittermination', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.AddField( + model_name='providernetwork', + name='service_id', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AlterField( + model_name='circuit', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='circuittermination', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='circuittype', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='provider', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='providernetwork', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='circuittermination', + name='_link_peer_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='circuit', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='circuittermination', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='circuittype', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='provider', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='providernetwork', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='provider', + name='asns', + field=models.ManyToManyField(blank=True, related_name='providers', to='ipam.asn'), + ), + migrations.AddField( + model_name='circuit', + name='termination_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='circuittermination', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), + ), + migrations.AddField( + model_name='circuittermination', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='circuittermination', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + ] diff --git a/netbox/circuits/migrations/0004_rename_cable_peer.py b/netbox/circuits/migrations/0004_rename_cable_peer.py deleted file mode 100644 index 81d507eb4..000000000 --- a/netbox/circuits/migrations/0004_rename_cable_peer.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0003_extend_tag_support'), - ] - - operations = [ - migrations.RenameField( - model_name='circuittermination', - old_name='_cable_peer_id', - new_name='_link_peer_id', - ), - migrations.RenameField( - model_name='circuittermination', - old_name='_cable_peer_type', - new_name='_link_peer_type', - ), - ] diff --git a/netbox/circuits/migrations/0032_provider_service_id.py b/netbox/circuits/migrations/0032_provider_service_id.py deleted file mode 100644 index 58936d1bd..000000000 --- a/netbox/circuits/migrations/0032_provider_service_id.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0004_rename_cable_peer'), - ('dcim', '0145_site_remove_deprecated_fields'), - ] - - operations = [ - migrations.AddField( - model_name='providernetwork', - name='service_id', - field=models.CharField(blank=True, max_length=100), - ), - ] diff --git a/netbox/circuits/migrations/0033_standardize_id_fields.py b/netbox/circuits/migrations/0033_standardize_id_fields.py deleted file mode 100644 index 475fc2527..000000000 --- a/netbox/circuits/migrations/0033_standardize_id_fields.py +++ /dev/null @@ -1,44 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0032_provider_service_id'), - ] - - operations = [ - # Model IDs - migrations.AlterField( - model_name='circuit', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='circuittermination', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='circuittype', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='provider', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='providernetwork', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - - # GFK IDs - migrations.AlterField( - model_name='circuittermination', - name='_link_peer_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/circuits/migrations/0034_created_datetimefield.py b/netbox/circuits/migrations/0034_created_datetimefield.py deleted file mode 100644 index 4af78c1a2..000000000 --- a/netbox/circuits/migrations/0034_created_datetimefield.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.0.2 on 2022-02-08 18:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0033_standardize_id_fields'), - ] - - operations = [ - migrations.AlterField( - model_name='circuit', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='circuittermination', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='circuittype', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='provider', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='providernetwork', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - ] diff --git a/netbox/circuits/migrations/0035_provider_asns.py b/netbox/circuits/migrations/0035_provider_asns.py deleted file mode 100644 index afb0da4d6..000000000 --- a/netbox/circuits/migrations/0035_provider_asns.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.0.3 on 2022-03-30 20:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0057_created_datetimefield'), - ('circuits', '0034_created_datetimefield'), - ] - - operations = [ - migrations.AddField( - model_name='provider', - name='asns', - field=models.ManyToManyField(blank=True, related_name='providers', to='ipam.asn'), - ), - ] diff --git a/netbox/circuits/migrations/0036_circuit_termination_date_tags_custom_fields.py b/netbox/circuits/migrations/0036_circuit_termination_date_tags_custom_fields.py deleted file mode 100644 index 96b2a9d97..000000000 --- a/netbox/circuits/migrations/0036_circuit_termination_date_tags_custom_fields.py +++ /dev/null @@ -1,28 +0,0 @@ -from utilities.json import CustomFieldJSONEncoder -from django.db import migrations, models -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0035_provider_asns'), - ] - - operations = [ - migrations.AddField( - model_name='circuit', - name='termination_date', - field=models.DateField(blank=True, null=True), - ), - migrations.AddField( - model_name='circuittermination', - name='custom_field_data', - field=models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder), - ), - migrations.AddField( - model_name='circuittermination', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - ] diff --git a/netbox/circuits/migrations/0037_new_cabling_models.py b/netbox/circuits/migrations/0037_new_cabling_models.py deleted file mode 100644 index ee08147f3..000000000 --- a/netbox/circuits/migrations/0037_new_cabling_models.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0036_circuit_termination_date_tags_custom_fields'), - ] - - operations = [ - migrations.AddField( - model_name='circuittermination', - name='cable_end', - field=models.CharField(blank=True, max_length=1), - ), - ] diff --git a/netbox/circuits/migrations/0038_cabling_cleanup.py b/netbox/circuits/migrations/0038_cabling_cleanup.py deleted file mode 100644 index 0672057e3..000000000 --- a/netbox/circuits/migrations/0038_cabling_cleanup.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0037_new_cabling_models'), - ('dcim', '0160_populate_cable_ends'), - ] - - operations = [ - migrations.RemoveField( - model_name='circuittermination', - name='_link_peer_id', - ), - migrations.RemoveField( - model_name='circuittermination', - name='_link_peer_type', - ), - ] diff --git a/netbox/circuits/migrations/0042_provideraccount.py b/netbox/circuits/migrations/0038_squashed_0042.py similarity index 50% rename from netbox/circuits/migrations/0042_provideraccount.py rename to netbox/circuits/migrations/0038_squashed_0042.py index 3e583844e..f57fde3db 100644 --- a/netbox/circuits/migrations/0042_provideraccount.py +++ b/netbox/circuits/migrations/0038_squashed_0042.py @@ -1,46 +1,83 @@ -from django.db import migrations, models import django.db.models.deletion import taggit.managers +from django.db import migrations, models + import utilities.json -def create_provideraccounts_from_providers(apps, schema_editor): - """ - Migrate Account in Provider model to separate account model - """ - Provider = apps.get_model('circuits', 'Provider') - ProviderAccount = apps.get_model('circuits', 'ProviderAccount') - - provider_accounts = [] - for provider in Provider.objects.all(): - if provider.account: - provider_accounts.append(ProviderAccount( - provider=provider, - account=provider.account - )) - ProviderAccount.objects.bulk_create(provider_accounts, batch_size=100) - - -def restore_providers_from_provideraccounts(apps, schema_editor): - """ - Restore Provider account values from auto-generated ProviderAccounts - """ - ProviderAccount = apps.get_model('circuits', 'ProviderAccount') - provider_accounts = ProviderAccount.objects.order_by('pk') - for provideraccount in provider_accounts: - if provider_accounts.filter(provider=provideraccount.provider)[0] == provideraccount: - provideraccount.provider.account = provideraccount.account - provideraccount.provider.save() - - class Migration(migrations.Migration): - dependencies = [ - ('extras', '0084_staging'), + replaces = [ + ('circuits', '0038_cabling_cleanup'), + ('circuits', '0039_unique_constraints'), + ('circuits', '0040_provider_remove_deprecated_fields'), ('circuits', '0041_standardize_description_comments'), + ('circuits', '0042_provideraccount') + ] + + dependencies = [ + ('circuits', '0037_new_cabling_models'), + ('dcim', '0160_populate_cable_ends'), ] operations = [ + migrations.RemoveField( + model_name='circuittermination', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='circuittermination', + name='_link_peer_type', + ), + migrations.RemoveConstraint( + model_name='providernetwork', + name='circuits_providernetwork_provider_name', + ), + migrations.AlterUniqueTogether( + name='circuit', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='circuittermination', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='providernetwork', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='circuit', + constraint=models.UniqueConstraint(fields=('provider', 'cid'), name='circuits_circuit_unique_provider_cid'), + ), + migrations.AddConstraint( + model_name='circuittermination', + constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'), + ), + migrations.AddConstraint( + model_name='providernetwork', + constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'), + ), + migrations.RemoveField( + model_name='provider', + name='admin_contact', + ), + migrations.RemoveField( + model_name='provider', + name='asn', + ), + migrations.RemoveField( + model_name='provider', + name='noc_contact', + ), + migrations.RemoveField( + model_name='provider', + name='portal_url', + ), + migrations.AddField( + model_name='provider', + name='description', + field=models.CharField(blank=True, max_length=200), + ), migrations.CreateModel( name='ProviderAccount', fields=[ @@ -67,9 +104,6 @@ class Migration(migrations.Migration): model_name='provideraccount', constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'), ), - migrations.RunPython( - create_provideraccounts_from_providers, restore_providers_from_provideraccounts - ), migrations.RemoveField( model_name='provider', name='account', @@ -77,7 +111,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='circuit', name='provider_account', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount', null=True, blank=True), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount'), preserve_default=False, ), migrations.AlterModelOptions( diff --git a/netbox/circuits/migrations/0039_unique_constraints.py b/netbox/circuits/migrations/0039_unique_constraints.py deleted file mode 100644 index 1d5b62499..000000000 --- a/netbox/circuits/migrations/0039_unique_constraints.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0038_cabling_cleanup'), - ] - - operations = [ - migrations.RemoveConstraint( - model_name='providernetwork', - name='circuits_providernetwork_provider_name', - ), - migrations.AlterUniqueTogether( - name='circuit', - unique_together=set(), - ), - migrations.AlterUniqueTogether( - name='circuittermination', - unique_together=set(), - ), - migrations.AlterUniqueTogether( - name='providernetwork', - unique_together=set(), - ), - migrations.AddConstraint( - model_name='circuit', - constraint=models.UniqueConstraint(fields=('provider', 'cid'), name='circuits_circuit_unique_provider_cid'), - ), - migrations.AddConstraint( - model_name='circuittermination', - constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'), - ), - migrations.AddConstraint( - model_name='providernetwork', - constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'), - ), - ] diff --git a/netbox/circuits/migrations/0040_provider_remove_deprecated_fields.py b/netbox/circuits/migrations/0040_provider_remove_deprecated_fields.py deleted file mode 100644 index 98c82204d..000000000 --- a/netbox/circuits/migrations/0040_provider_remove_deprecated_fields.py +++ /dev/null @@ -1,59 +0,0 @@ -import os - -from django.db import migrations -from django.db.utils import DataError - - -def check_legacy_data(apps, schema_editor): - """ - Abort the migration if any legacy provider fields still contain data. - """ - Provider = apps.get_model('circuits', 'Provider') - - provider_count = Provider.objects.exclude(asn__isnull=True).count() - if provider_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ: - raise DataError( - f"Unable to proceed with deleting asn field from Provider model: Found {provider_count} " - f"providers with legacy ASN data. Please ensure all legacy provider ASN data has been " - f"migrated to ASN objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA " - f"environment variable to bypass this safeguard and delete all legacy provider ASN data." - ) - - provider_count = Provider.objects.exclude(admin_contact='', noc_contact='', portal_url='').count() - if provider_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ: - raise DataError( - f"Unable to proceed with deleting contact fields from Provider model: Found {provider_count} " - f"providers with legacy contact data. Please ensure all legacy provider contact data has been " - f"migrated to contact objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA " - f"environment variable to bypass this safeguard and delete all legacy provider contact data." - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0039_unique_constraints'), - ] - - operations = [ - migrations.RunPython( - code=check_legacy_data, - reverse_code=migrations.RunPython.noop - ), - migrations.RemoveField( - model_name='provider', - name='admin_contact', - ), - migrations.RemoveField( - model_name='provider', - name='asn', - ), - migrations.RemoveField( - model_name='provider', - name='noc_contact', - ), - migrations.RemoveField( - model_name='provider', - name='portal_url', - ), - ] diff --git a/netbox/circuits/migrations/0041_standardize_description_comments.py b/netbox/circuits/migrations/0041_standardize_description_comments.py deleted file mode 100644 index 49cdefcba..000000000 --- a/netbox/circuits/migrations/0041_standardize_description_comments.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.2 on 2022-11-03 18:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0040_provider_remove_deprecated_fields'), - ] - - operations = [ - migrations.AddField( - model_name='provider', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - ] diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py index 4ae426df5..a16a06d62 100644 --- a/netbox/core/api/serializers.py +++ b/netbox/core/api/serializers.py @@ -36,7 +36,7 @@ class DataSourceSerializer(NetBoxModelSerializer): model = DataSource fields = [ 'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments', - 'parameters', 'ignore_rules', 'created', 'last_updated', 'file_count', + 'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count', ] diff --git a/netbox/core/constants.py b/netbox/core/constants.py new file mode 100644 index 000000000..3c3382dcc --- /dev/null +++ b/netbox/core/constants.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass + +from django.utils.translation import gettext_lazy as _ +from rq.job import JobStatus + +__all__ = ( + 'RQ_TASK_STATUSES', +) + + +@dataclass +class Status: + label: str + color: str + + +RQ_TASK_STATUSES = { + JobStatus.QUEUED: Status(_('Queued'), 'cyan'), + JobStatus.FINISHED: Status(_('Finished'), 'green'), + JobStatus.FAILED: Status(_('Failed'), 'red'), + JobStatus.STARTED: Status(_('Started'), 'blue'), + JobStatus.DEFERRED: Status(_('Deferred'), 'gray'), + JobStatus.SCHEDULED: Status(_('Scheduled'), 'purple'), + JobStatus.STOPPED: Status(_('Stopped'), 'orange'), + JobStatus.CANCELED: Status(_('Cancelled'), 'yellow'), +} diff --git a/netbox/core/forms/bulk_edit.py b/netbox/core/forms/bulk_edit.py index dcc92c6f0..bc2ef8fc9 100644 --- a/netbox/core/forms/bulk_edit.py +++ b/netbox/core/forms/bulk_edit.py @@ -21,7 +21,7 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm): enabled = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect(), - label=_('Enforce unique space') + label=_('Enabled') ) description = forms.CharField( label=_('Description'), diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 652728734..52fa3a608 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -11,7 +11,7 @@ from netbox.config import get_config, PARAMS from netbox.forms import NetBoxModelForm from netbox.registry import registry from netbox.utils import get_data_backend_choices -from utilities.forms import BootstrapMixin, get_field_value +from utilities.forms import get_field_value from utilities.forms.fields import CommentField from utilities.forms.widgets import HTMXSelect @@ -138,7 +138,7 @@ class ConfigFormMetaclass(forms.models.ModelFormMetaclass): return super().__new__(mcs, name, bases, attrs) -class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass): +class ConfigRevisionForm(forms.ModelForm, metaclass=ConfigFormMetaclass): """ Form for creating a new ConfigRevision. """ diff --git a/netbox/core/management/commands/makemigrations.py b/netbox/core/management/commands/makemigrations.py index ce40bd3cc..afab5077d 100644 --- a/netbox/core/management/commands/makemigrations.py +++ b/netbox/core/management/commands/makemigrations.py @@ -9,9 +9,9 @@ class Command(_Command): """ This built-in management command enables the creation of new database schema migration files, which should never be required by and ordinary user. We prevent this command from executing unless the configuration - indicates that the user is a developer (i.e. configuration.DEVELOPER == True). + indicates that the user is a developer (i.e. configuration.DEVELOPER == True), or it was run with --check. """ - if not settings.DEVELOPER: + if not kwargs['check_changes'] and not settings.DEVELOPER: raise CommandError( "This command is available for development purposes only. It will\n" "NOT resolve any issues with missing or unapplied migrations. For assistance,\n" diff --git a/netbox/core/migrations/0001_initial.py b/netbox/core/migrations/0001_squashed_0005.py similarity index 51% rename from netbox/core/migrations/0001_initial.py rename to netbox/core/migrations/0001_squashed_0005.py index 775a5dcb1..971370bf2 100644 --- a/netbox/core/migrations/0001_initial.py +++ b/netbox/core/migrations/0001_squashed_0005.py @@ -1,18 +1,26 @@ -# Generated by Django 4.1.5 on 2023-02-02 02:37 - import django.core.validators -from django.db import migrations, models import django.db.models.deletion import taggit.managers +from django.conf import settings +from django.db import migrations, models + import utilities.json class Migration(migrations.Migration): - initial = True + replaces = [ + ('core', '0001_initial'), + ('core', '0002_managedfile'), + ('core', '0003_job'), + ('core', '0004_replicate_jobresults'), + ('core', '0005_job_created_auto_now') + ] dependencies = [ - ('extras', '0084_staging'), + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('extras', '0002_squashed_0059'), ] operations = [ @@ -71,13 +79,61 @@ class Migration(migrations.Migration): ('datafile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='core.datafile')), ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), ], - ), - migrations.AddIndex( - model_name='autosyncrecord', - index=models.Index(fields=['object_type', 'object_id'], name='core_autosy_object__c17bac_idx'), + options={ + 'indexes': [models.Index(fields=['object_type', 'object_id'], name='core_autosy_object__c17bac_idx')], + }, ), migrations.AddConstraint( model_name='autosyncrecord', constraint=models.UniqueConstraint(fields=('object_type', 'object_id'), name='core_autosyncrecord_object'), ), + migrations.CreateModel( + name='ManagedFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('data_path', models.CharField(blank=True, editable=False, max_length=1000)), + ('data_synced', models.DateTimeField(blank=True, editable=False, null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('last_updated', models.DateTimeField(blank=True, editable=False, null=True)), + ('file_root', models.CharField(max_length=1000)), + ('file_path', models.FilePathField(editable=False)), + ('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')), + ('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')), + ('auto_sync_enabled', models.BooleanField(default=False)), + ], + options={ + 'ordering': ('file_root', 'file_path'), + 'indexes': [models.Index(fields=['file_root', 'file_path'], name='core_managedfile_root_path')], + }, + ), + migrations.AddConstraint( + model_name='managedfile', + constraint=models.UniqueConstraint(fields=('file_root', 'file_path'), name='core_managedfile_unique_root_path'), + ), + migrations.CreateModel( + name='Job', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('object_id', models.PositiveBigIntegerField(blank=True, null=True)), + ('name', models.CharField(max_length=200)), + ('created', models.DateTimeField()), + ('scheduled', models.DateTimeField(blank=True, null=True)), + ('interval', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)])), + ('started', models.DateTimeField(blank=True, null=True)), + ('completed', models.DateTimeField(blank=True, null=True)), + ('status', models.CharField(default='pending', max_length=30)), + ('data', models.JSONField(blank=True, null=True)), + ('job_id', models.UUIDField(unique=True)), + ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created'], + }, + ), + migrations.AlterField( + model_name='job', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), ] diff --git a/netbox/core/migrations/0002_managedfile.py b/netbox/core/migrations/0002_managedfile.py deleted file mode 100644 index 169063be8..000000000 --- a/netbox/core/migrations/0002_managedfile.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-23 17:35 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='ManagedFile', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('data_path', models.CharField(blank=True, editable=False, max_length=1000)), - ('data_synced', models.DateTimeField(blank=True, editable=False, null=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('last_updated', models.DateTimeField(blank=True, editable=False, null=True)), - ('file_root', models.CharField(max_length=1000)), - ('file_path', models.FilePathField(editable=False)), - ('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')), - ('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')), - ('auto_sync_enabled', models.BooleanField(default=False)), - ], - options={ - 'ordering': ('file_root', 'file_path'), - }, - ), - migrations.AddIndex( - model_name='managedfile', - index=models.Index(fields=['file_root', 'file_path'], name='core_managedfile_root_path'), - ), - migrations.AddConstraint( - model_name='managedfile', - constraint=models.UniqueConstraint(fields=('file_root', 'file_path'), name='core_managedfile_unique_root_path'), - ), - ] diff --git a/netbox/core/migrations/0003_job.py b/netbox/core/migrations/0003_job.py deleted file mode 100644 index f2fe41afb..000000000 --- a/netbox/core/migrations/0003_job.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-27 15:02 - -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('core', '0002_managedfile'), - ] - - operations = [ - migrations.CreateModel( - name='Job', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('object_id', models.PositiveBigIntegerField(blank=True, null=True)), - ('name', models.CharField(max_length=200)), - ('created', models.DateTimeField()), - ('scheduled', models.DateTimeField(blank=True, null=True)), - ('interval', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)])), - ('started', models.DateTimeField(blank=True, null=True)), - ('completed', models.DateTimeField(blank=True, null=True)), - ('status', models.CharField(default='pending', max_length=30)), - ('data', models.JSONField(blank=True, null=True)), - ('job_id', models.UUIDField(unique=True)), - ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['-created'], - }, - ), - ] diff --git a/netbox/core/migrations/0004_replicate_jobresults.py b/netbox/core/migrations/0004_replicate_jobresults.py deleted file mode 100644 index 881506b9b..000000000 --- a/netbox/core/migrations/0004_replicate_jobresults.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.db import migrations - - -def replicate_jobresults(apps, schema_editor): - """ - Replicate existing JobResults to the new Jobs table before deleting the old JobResults table. - """ - Job = apps.get_model('core', 'Job') - JobResult = apps.get_model('extras', 'JobResult') - - jobs = [] - for job_result in JobResult.objects.order_by('pk').iterator(chunk_size=100): - jobs.append( - Job( - object_type=job_result.obj_type, - name=job_result.name, - created=job_result.created, - scheduled=job_result.scheduled, - interval=job_result.interval, - started=job_result.started, - completed=job_result.completed, - user=job_result.user, - status=job_result.status, - data=job_result.data, - job_id=job_result.job_id, - ) - ) - if len(jobs) == 100: - Job.objects.bulk_create(jobs) - jobs = [] - if jobs: - Job.objects.bulk_create(jobs) - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0003_job'), - ] - - operations = [ - migrations.RunPython( - code=replicate_jobresults, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/core/migrations/0005_job_created_auto_now.py b/netbox/core/migrations/0005_job_created_auto_now.py deleted file mode 100644 index 12fd526ef..000000000 --- a/netbox/core/migrations/0005_job_created_auto_now.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-27 17:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0004_replicate_jobresults'), - ] - - operations = [ - migrations.AlterField( - model_name='job', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - ] diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index efda879af..6597a4b4d 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -14,6 +14,7 @@ from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ +from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED from netbox.models import PrimaryModel from netbox.models.features import JobsMixin from netbox.registry import registry @@ -130,6 +131,28 @@ class DataSource(JobsMixin, PrimaryModel): 'source_url': f"URLs for local sources must start with file:// (or specify no scheme)" }) + def to_objectchange(self, action): + objectchange = super().to_objectchange(action) + + # Censor any backend parameters marked as sensitive in the serialized data + pre_change_params = {} + post_change_params = {} + if objectchange.prechange_data: + pre_change_params = objectchange.prechange_data.get('parameters') or {} # parameters may be None + if objectchange.postchange_data: + post_change_params = objectchange.postchange_data.get('parameters') or {} + for param in self.backend_class.sensitive_parameters: + if post_change_params.get(param): + if post_change_params[param] != pre_change_params.get(param): + # Set the "changed" token if the parameter's value has been modified + post_change_params[param] = CENSOR_TOKEN_CHANGED + else: + post_change_params[param] = CENSOR_TOKEN + if pre_change_params.get(param): + pre_change_params[param] = CENSOR_TOKEN + + return objectchange + def enqueue_sync_job(self, request): """ Enqueue a background job to synchronize the DataSource by calling sync(). diff --git a/netbox/core/tables/__init__.py b/netbox/core/tables/__init__.py index 69f9d8a48..8f219afa4 100644 --- a/netbox/core/tables/__init__.py +++ b/netbox/core/tables/__init__.py @@ -1,3 +1,5 @@ from .config import * from .data import * from .jobs import * +from .tasks import * +from .plugins import * diff --git a/netbox/core/tables/columns.py b/netbox/core/tables/columns.py index 93f1e3901..f3d985bc3 100644 --- a/netbox/core/tables/columns.py +++ b/netbox/core/tables/columns.py @@ -1,9 +1,12 @@ import django_tables2 as tables +from django.utils.safestring import mark_safe +from core.constants import RQ_TASK_STATUSES from netbox.registry import registry __all__ = ( 'BackendTypeColumn', + 'RQJobStatusColumn', ) @@ -18,3 +21,16 @@ class BackendTypeColumn(tables.Column): def value(self, value): return value + + +class RQJobStatusColumn(tables.Column): + """ + Render a colored label for the status of an RQ job. + """ + def render(self, value): + status = RQ_TASK_STATUSES.get(value) + return mark_safe(f'{status.label}') + + def value(self, value): + status = RQ_TASK_STATUSES.get(value) + return status.label diff --git a/netbox/core/tables/plugins.py b/netbox/core/tables/plugins.py new file mode 100644 index 000000000..2e3c0a991 --- /dev/null +++ b/netbox/core/tables/plugins.py @@ -0,0 +1,39 @@ +import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ +from netbox.tables import BaseTable + +__all__ = ( + 'PluginTable', +) + + +class PluginTable(BaseTable): + name = tables.Column( + accessor=tables.A('verbose_name'), + verbose_name=_('Name') + ) + version = tables.Column( + verbose_name=_('Version') + ) + package = tables.Column( + accessor=tables.A('name'), + verbose_name=_('Package') + ) + author = tables.Column( + verbose_name=_('Author') + ) + author_email = tables.Column( + verbose_name=_('Author Email') + ) + description = tables.Column( + verbose_name=_('Description') + ) + + class Meta(BaseTable.Meta): + empty_text = _('No plugins found') + fields = ( + 'name', 'version', 'package', 'author', 'author_email', 'description', + ) + default_columns = ( + 'name', 'version', 'package', 'author', 'author_email', 'description', + ) diff --git a/netbox/core/tables/tasks.py b/netbox/core/tables/tasks.py new file mode 100644 index 000000000..531ec6375 --- /dev/null +++ b/netbox/core/tables/tasks.py @@ -0,0 +1,134 @@ +import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ +from django_tables2.utils import A + +from core.tables.columns import RQJobStatusColumn +from netbox.tables import BaseTable + + +class BackgroundQueueTable(BaseTable): + name = tables.Column( + verbose_name=_("Name") + ) + jobs = tables.Column( + linkify=("core:background_task_list", [A("index"), "queued"]), + verbose_name=_("Queued") + ) + oldest_job_timestamp = tables.Column( + verbose_name=_("Oldest Task") + ) + started_jobs = tables.Column( + linkify=("core:background_task_list", [A("index"), "started"]), + verbose_name=_("Active") + ) + deferred_jobs = tables.Column( + linkify=("core:background_task_list", [A("index"), "deferred"]), + verbose_name=_("Deferred") + ) + finished_jobs = tables.Column( + linkify=("core:background_task_list", [A("index"), "finished"]), + verbose_name=_("Finished") + ) + failed_jobs = tables.Column( + linkify=("core:background_task_list", [A("index"), "failed"]), + verbose_name=_("Failed") + ) + scheduled_jobs = tables.Column( + linkify=("core:background_task_list", [A("index"), "scheduled"]), + verbose_name=_("Scheduled") + ) + workers = tables.Column( + linkify=("core:worker_list", [A("index")]), + verbose_name=_("Workers") + ) + host = tables.Column( + accessor="connection_kwargs__host", + verbose_name=_("Host") + ) + port = tables.Column( + accessor="connection_kwargs__port", + verbose_name=_("Port") + ) + db = tables.Column( + accessor="connection_kwargs__db", + verbose_name=_("DB") + ) + pid = tables.Column( + accessor="scheduler__pid", + verbose_name=_("Scheduler PID") + ) + + class Meta(BaseTable.Meta): + empty_text = _('No queues found') + fields = ( + 'name', 'jobs', 'oldest_job_timestamp', 'started_jobs', 'deferred_jobs', 'finished_jobs', 'failed_jobs', + 'scheduled_jobs', 'workers', 'host', 'port', 'db', 'pid', + ) + default_columns = ( + 'name', 'jobs', 'started_jobs', 'deferred_jobs', 'finished_jobs', 'failed_jobs', 'scheduled_jobs', + 'workers', + ) + + +class BackgroundTaskTable(BaseTable): + id = tables.Column( + linkify=("core:background_task", [A("id")]), + verbose_name=_("ID") + ) + created_at = tables.DateTimeColumn( + verbose_name=_("Created") + ) + enqueued_at = tables.DateTimeColumn( + verbose_name=_("Enqueued") + ) + ended_at = tables.DateTimeColumn( + verbose_name=_("Ended") + ) + status = RQJobStatusColumn( + verbose_name=_("Status"), + accessor='get_status' + ) + callable = tables.Column( + empty_values=(), + verbose_name=_("Callable") + ) + + class Meta(BaseTable.Meta): + empty_text = _('No tasks found') + fields = ( + 'id', 'created_at', 'enqueued_at', 'ended_at', 'status', 'callable', + ) + default_columns = ( + 'id', 'created_at', 'enqueued_at', 'ended_at', 'status', 'callable', + ) + + def render_callable(self, value, record): + try: + return record.func_name + except Exception as e: + return repr(e) + + +class WorkerTable(BaseTable): + name = tables.Column( + linkify=("core:worker", [A("name")]), + verbose_name=_("Name") + ) + state = tables.Column( + verbose_name=_("State") + ) + birth_date = tables.DateTimeColumn( + verbose_name=_("Birth") + ) + pid = tables.Column( + verbose_name=_("PID") + ) + + class Meta(BaseTable.Meta): + empty_text = _('No workers found') + fields = ( + 'name', 'state', 'birth_date', 'pid', + ) + default_columns = ( + 'name', 'state', 'birth_date', 'pid', + ) diff --git a/netbox/core/tests/test_filtersets.py b/netbox/core/tests/test_filtersets.py index e6e52a8b3..8ff104142 100644 --- a/netbox/core/tests/test_filtersets.py +++ b/netbox/core/tests/test_filtersets.py @@ -1,8 +1,6 @@ -from datetime import datetime +from datetime import datetime, timezone from django.test import TestCase -from django.utils import timezone - from utilities.testing import ChangeLoggedFilterSetTests from ..choices import * from ..filtersets import * diff --git a/netbox/core/tests/test_models.py b/netbox/core/tests/test_models.py new file mode 100644 index 000000000..0eeb66984 --- /dev/null +++ b/netbox/core/tests/test_models.py @@ -0,0 +1,122 @@ +from django.test import TestCase + +from core.models import DataSource +from extras.choices import ObjectChangeActionChoices +from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED + + +class DataSourceChangeLoggingTestCase(TestCase): + + def test_password_added_on_create(self): + datasource = DataSource.objects.create( + name='Data Source 1', + type='git', + source_url='http://localhost/', + parameters={ + 'username': 'jeff', + 'password': 'foobar123', + } + ) + + objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_CREATE) + self.assertIsNone(objectchange.prechange_data) + self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff') + self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN_CHANGED) + + def test_password_added_on_update(self): + datasource = DataSource.objects.create( + name='Data Source 1', + type='git', + source_url='http://localhost/' + ) + datasource.snapshot() + + # Add a blank password + datasource.parameters = { + 'username': 'jeff', + 'password': '', + } + + objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE) + self.assertIsNone(objectchange.prechange_data['parameters']) + self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff') + self.assertEqual(objectchange.postchange_data['parameters']['password'], '') + + # Add a password + datasource.parameters = { + 'username': 'jeff', + 'password': 'foobar123', + } + + objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE) + self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff') + self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN_CHANGED) + + def test_password_changed(self): + datasource = DataSource.objects.create( + name='Data Source 1', + type='git', + source_url='http://localhost/', + parameters={ + 'username': 'jeff', + 'password': 'password1', + } + ) + datasource.snapshot() + + # Change the password + datasource.parameters['password'] = 'password2' + + objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE) + self.assertEqual(objectchange.prechange_data['parameters']['username'], 'jeff') + self.assertEqual(objectchange.prechange_data['parameters']['password'], CENSOR_TOKEN) + self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff') + self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN_CHANGED) + + def test_password_removed_on_update(self): + datasource = DataSource.objects.create( + name='Data Source 1', + type='git', + source_url='http://localhost/', + parameters={ + 'username': 'jeff', + 'password': 'foobar123', + } + ) + datasource.snapshot() + + objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE) + self.assertEqual(objectchange.prechange_data['parameters']['username'], 'jeff') + self.assertEqual(objectchange.prechange_data['parameters']['password'], CENSOR_TOKEN) + self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff') + self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN) + + # Remove the password + datasource.parameters['password'] = '' + + objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE) + self.assertEqual(objectchange.prechange_data['parameters']['username'], 'jeff') + self.assertEqual(objectchange.prechange_data['parameters']['password'], CENSOR_TOKEN) + self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff') + self.assertEqual(objectchange.postchange_data['parameters']['password'], '') + + def test_password_not_modified(self): + datasource = DataSource.objects.create( + name='Data Source 1', + type='git', + source_url='http://localhost/', + parameters={ + 'username': 'username1', + 'password': 'foobar123', + } + ) + datasource.snapshot() + + # Remove the password + datasource.parameters['username'] = 'username2' + + objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE) + self.assertEqual(objectchange.prechange_data['parameters']['username'], 'username1') + self.assertEqual(objectchange.prechange_data['parameters']['password'], CENSOR_TOKEN) + self.assertEqual(objectchange.postchange_data['parameters']['username'], 'username2') + self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN) diff --git a/netbox/core/tests/test_views.py b/netbox/core/tests/test_views.py index 16d07f376..b7a951a0f 100644 --- a/netbox/core/tests/test_views.py +++ b/netbox/core/tests/test_views.py @@ -1,6 +1,16 @@ -from django.utils import timezone +import logging +import uuid +from datetime import datetime -from utilities.testing import ViewTestCases, create_tags +from django.urls import reverse +from django.utils import timezone +from django_rq import get_queue +from django_rq.settings import QUEUES_MAP +from django_rq.workers import get_worker +from rq.job import Job as RQ_Job, JobStatus +from rq.registry import DeferredJobRegistry, FailedJobRegistry, FinishedJobRegistry, StartedJobRegistry + +from utilities.testing import TestCase, ViewTestCases, create_tags from ..models import * @@ -87,3 +97,211 @@ class DataFileTestCase( ), ) DataFile.objects.bulk_create(data_files) + + +class BackgroundTaskTestCase(TestCase): + user_permissions = () + + # Dummy worker functions + @staticmethod + def dummy_job_default(): + return "Job finished" + + @staticmethod + def dummy_job_high(): + return "Job finished" + + @staticmethod + def dummy_job_failing(): + raise Exception("Job failed") + + def setUp(self): + super().setUp() + self.user.is_staff = True + self.user.is_active = True + self.user.save() + + # Clear all queues prior to running each test + get_queue('default').connection.flushall() + get_queue('high').connection.flushall() + get_queue('low').connection.flushall() + + def test_background_queue_list(self): + url = reverse('core:background_queue_list') + + # Attempt to load view without permission + self.user.is_staff = False + self.user.save() + response = self.client.get(url) + self.assertEqual(response.status_code, 403) + + # Load view with permission + self.user.is_staff = True + self.user.save() + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn('default', str(response.content)) + self.assertIn('high', str(response.content)) + self.assertIn('low', str(response.content)) + + def test_background_tasks_list_default(self): + queue = get_queue('default') + queue.enqueue(self.dummy_job_default) + queue_index = QUEUES_MAP['default'] + + response = self.client.get(reverse('core:background_task_list', args=[queue_index, 'queued'])) + self.assertEqual(response.status_code, 200) + self.assertIn('BackgroundTaskTestCase.dummy_job_default', str(response.content)) + + def test_background_tasks_list_high(self): + queue = get_queue('high') + queue.enqueue(self.dummy_job_high) + queue_index = QUEUES_MAP['high'] + + response = self.client.get(reverse('core:background_task_list', args=[queue_index, 'queued'])) + self.assertEqual(response.status_code, 200) + self.assertIn('BackgroundTaskTestCase.dummy_job_high', str(response.content)) + + def test_background_tasks_list_finished(self): + queue = get_queue('default') + job = queue.enqueue(self.dummy_job_default) + queue_index = QUEUES_MAP['default'] + + registry = FinishedJobRegistry(queue.name, queue.connection) + registry.add(job, 2) + response = self.client.get(reverse('core:background_task_list', args=[queue_index, 'finished'])) + self.assertEqual(response.status_code, 200) + self.assertIn('BackgroundTaskTestCase.dummy_job_default', str(response.content)) + + def test_background_tasks_list_failed(self): + queue = get_queue('default') + job = queue.enqueue(self.dummy_job_default) + queue_index = QUEUES_MAP['default'] + + registry = FailedJobRegistry(queue.name, queue.connection) + registry.add(job, 2) + response = self.client.get(reverse('core:background_task_list', args=[queue_index, 'failed'])) + self.assertEqual(response.status_code, 200) + self.assertIn('BackgroundTaskTestCase.dummy_job_default', str(response.content)) + + def test_background_tasks_scheduled(self): + queue = get_queue('default') + queue.enqueue_at(datetime.now(), self.dummy_job_default) + queue_index = QUEUES_MAP['default'] + + response = self.client.get(reverse('core:background_task_list', args=[queue_index, 'scheduled'])) + self.assertEqual(response.status_code, 200) + self.assertIn('BackgroundTaskTestCase.dummy_job_default', str(response.content)) + + def test_background_tasks_list_deferred(self): + queue = get_queue('default') + job = queue.enqueue(self.dummy_job_default) + queue_index = QUEUES_MAP['default'] + + registry = DeferredJobRegistry(queue.name, queue.connection) + registry.add(job, 2) + response = self.client.get(reverse('core:background_task_list', args=[queue_index, 'deferred'])) + self.assertEqual(response.status_code, 200) + self.assertIn('BackgroundTaskTestCase.dummy_job_default', str(response.content)) + + def test_background_task(self): + queue = get_queue('default') + job = queue.enqueue(self.dummy_job_default) + + response = self.client.get(reverse('core:background_task', args=[job.id])) + self.assertEqual(response.status_code, 200) + self.assertIn('Background Tasks', str(response.content)) + self.assertIn(str(job.id), str(response.content)) + self.assertIn('Callable', str(response.content)) + self.assertIn('Meta', str(response.content)) + self.assertIn('Keyword Arguments', str(response.content)) + + def test_background_task_delete(self): + queue = get_queue('default') + job = queue.enqueue(self.dummy_job_default) + + response = self.client.post(reverse('core:background_task_delete', args=[job.id]), {'confirm': True}) + self.assertEqual(response.status_code, 302) + self.assertFalse(RQ_Job.exists(job.id, connection=queue.connection)) + self.assertNotIn(job.id, queue.job_ids) + + def test_background_task_requeue(self): + queue = get_queue('default') + + # Enqueue & run a job that will fail + job = queue.enqueue(self.dummy_job_failing) + worker = get_worker('default') + worker.work(burst=True) + self.assertTrue(job.is_failed) + + # Re-enqueue the failed job and check that its status has been reset + response = self.client.get(reverse('core:background_task_requeue', args=[job.id])) + self.assertEqual(response.status_code, 302) + self.assertFalse(job.is_failed) + + def test_background_task_enqueue(self): + queue = get_queue('default') + + # Enqueue some jobs that each depends on its predecessor + job = previous_job = None + for _ in range(0, 3): + job = queue.enqueue(self.dummy_job_default, depends_on=previous_job) + previous_job = job + + # Check that the last job to be enqueued has a status of deferred + self.assertIsNotNone(job) + self.assertEqual(job.get_status(), JobStatus.DEFERRED) + self.assertIsNone(job.enqueued_at) + + # Force-enqueue the deferred job + response = self.client.get(reverse('core:background_task_enqueue', args=[job.id])) + self.assertEqual(response.status_code, 302) + + # Check that job's status is updated correctly + job = queue.fetch_job(job.id) + self.assertEqual(job.get_status(), JobStatus.QUEUED) + self.assertIsNotNone(job.enqueued_at) + + def test_background_task_stop(self): + queue = get_queue('default') + + worker = get_worker('default') + job = queue.enqueue(self.dummy_job_default) + worker.prepare_job_execution(job) + + self.assertEqual(job.get_status(), JobStatus.STARTED) + + # Stop those jobs using the view + started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection) + self.assertEqual(len(started_job_registry), 1) + response = self.client.get(reverse('core:background_task_stop', args=[job.id])) + self.assertEqual(response.status_code, 302) + worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started + self.assertEqual(len(started_job_registry), 0) + + canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection) + self.assertEqual(len(canceled_job_registry), 1) + self.assertIn(job.id, canceled_job_registry) + + def test_worker_list(self): + worker1 = get_worker('default', name=uuid.uuid4().hex) + worker1.register_birth() + + worker2 = get_worker('high') + worker2.register_birth() + + queue_index = QUEUES_MAP['default'] + response = self.client.get(reverse('core:worker_list', args=[queue_index])) + self.assertEqual(response.status_code, 200) + self.assertIn(str(worker1.name), str(response.content)) + self.assertNotIn(str(worker2.name), str(response.content)) + + def test_worker(self): + worker1 = get_worker('default', name=uuid.uuid4().hex) + worker1.register_birth() + + response = self.client.get(reverse('core:worker', args=[worker1.name])) + self.assertEqual(response.status_code, 200) + self.assertIn(str(worker1.name), str(response.content)) + self.assertIn('Birth', str(response.content)) + self.assertIn('Total working time', str(response.content)) diff --git a/netbox/core/urls.py b/netbox/core/urls.py index 77c0d3194..bac2eed37 100644 --- a/netbox/core/urls.py +++ b/netbox/core/urls.py @@ -25,6 +25,17 @@ urlpatterns = ( path('jobs//', views.JobView.as_view(), name='job'), path('jobs//delete/', views.JobDeleteView.as_view(), name='job_delete'), + # Background Tasks + path('background-queues/', views.BackgroundQueueListView.as_view(), name='background_queue_list'), + path('background-queues///', views.BackgroundTaskListView.as_view(), name='background_task_list'), + path('background-tasks//', views.BackgroundTaskView.as_view(), name='background_task'), + path('background-tasks//delete/', views.BackgroundTaskDeleteView.as_view(), name='background_task_delete'), + path('background-tasks//requeue/', views.BackgroundTaskRequeueView.as_view(), name='background_task_requeue'), + path('background-tasks//enqueue/', views.BackgroundTaskEnqueueView.as_view(), name='background_task_enqueue'), + path('background-tasks//stop/', views.BackgroundTaskStopView.as_view(), name='background_task_stop'), + path('background-workers//', views.WorkerListView.as_view(), name='worker_list'), + path('background-workers//', views.WorkerView.as_view(), name='worker'), + # Config revisions path('config-revisions/', views.ConfigRevisionListView.as_view(), name='configrevision_list'), path('config-revisions/add/', views.ConfigRevisionEditView.as_view(), name='configrevision_add'), @@ -35,4 +46,6 @@ urlpatterns = ( # Configuration path('config/', views.ConfigView.as_view(), name='config'), + # Plugins + path('plugins/', views.PluginListView.as_view(), name='plugin_list'), ) diff --git a/netbox/core/views.py b/netbox/core/views.py index 537c33d9d..5662b126e 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -1,12 +1,30 @@ +from django.apps import apps +from django.conf import settings from django.contrib import messages +from django.contrib.auth.mixins import UserPassesTestMixin from django.core.cache import cache -from django.http import HttpResponseForbidden +from django.http import HttpResponseForbidden, Http404 from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from django.views.generic import View +from django_rq.queues import get_queue_by_index, get_redis_connection +from django_rq.settings import QUEUES_MAP, QUEUES_LIST +from django_rq.utils import get_jobs, get_statistics, stop_jobs +from rq import requeue_job +from rq.exceptions import NoSuchJobError +from rq.job import Job as RQ_Job, JobStatus as RQJobStatus +from rq.registry import ( + DeferredJobRegistry, FailedJobRegistry, FinishedJobRegistry, ScheduledJobRegistry, StartedJobRegistry, +) +from rq.worker import Worker +from rq.worker_registration import clean_worker_registry from netbox.config import get_config, PARAMS from netbox.views import generic from netbox.views.generic.base import BaseObjectView +from netbox.views.generic.mixins import TableMixin +from utilities.forms import ConfirmationForm from utilities.utils import count_related from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from . import filtersets, forms, tables @@ -232,3 +250,297 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View): messages.success(request, f"Restored configuration revision #{pk}") return redirect(candidate_config.get_absolute_url()) + + +# +# Background Tasks (RQ) +# + +class BaseRQView(UserPassesTestMixin, View): + + def test_func(self): + return self.request.user.is_staff + + +class BackgroundQueueListView(TableMixin, BaseRQView): + table = tables.BackgroundQueueTable + + def get(self, request): + data = get_statistics(run_maintenance_tasks=True)["queues"] + table = self.get_table(data, request, bulk_actions=False) + + return render(request, 'core/rq_queue_list.html', { + 'table': table, + }) + + +class BackgroundTaskListView(TableMixin, BaseRQView): + table = tables.BackgroundTaskTable + + def get_table_data(self, request, queue, status): + jobs = [] + + # Call get_jobs() to returned queued tasks + if status == RQJobStatus.QUEUED: + return queue.get_jobs() + + # For other statuses, determine the registry to list (or raise a 404 for invalid statuses) + try: + registry_cls = { + RQJobStatus.STARTED: StartedJobRegistry, + RQJobStatus.DEFERRED: DeferredJobRegistry, + RQJobStatus.FINISHED: FinishedJobRegistry, + RQJobStatus.FAILED: FailedJobRegistry, + RQJobStatus.SCHEDULED: ScheduledJobRegistry, + }[status] + except KeyError: + raise Http404 + registry = registry_cls(queue.name, queue.connection) + + job_ids = registry.get_job_ids() + if status != RQJobStatus.DEFERRED: + jobs = get_jobs(queue, job_ids, registry) + else: + # Deferred jobs require special handling + for job_id in job_ids: + try: + jobs.append(RQ_Job.fetch(job_id, connection=queue.connection, serializer=queue.serializer)) + except NoSuchJobError: + pass + + if jobs and status == RQJobStatus.SCHEDULED: + for job in jobs: + job.scheduled_at = registry.get_scheduled_time(job) + + return jobs + + def get(self, request, queue_index, status): + queue = get_queue_by_index(queue_index) + data = self.get_table_data(request, queue, status) + table = self.get_table(data, request, False) + + # If this is an HTMX request, return only the rendered table HTML + if request.htmx: + return render(request, 'htmx/table.html', { + 'table': table, + }) + + return render(request, 'core/rq_task_list.html', { + 'table': table, + 'queue': queue, + 'status': status, + }) + + +class BackgroundTaskView(BaseRQView): + + def get(self, request, job_id): + # all the RQ queues should use the same connection + config = QUEUES_LIST[0] + try: + job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),) + except NoSuchJobError: + raise Http404(_("Job {job_id} not found").format(job_id=job_id)) + + queue_index = QUEUES_MAP[job.origin] + queue = get_queue_by_index(queue_index) + + try: + exc_info = job._exc_info + except AttributeError: + exc_info = None + + return render(request, 'core/rq_task.html', { + 'queue': queue, + 'job': job, + 'queue_index': queue_index, + 'dependency_id': job._dependency_id, + 'exc_info': exc_info, + }) + + +class BackgroundTaskDeleteView(BaseRQView): + + def get(self, request, job_id): + if not request.htmx: + return redirect(reverse('core:background_queue_list')) + + form = ConfirmationForm(initial=request.GET) + + return render(request, 'htmx/delete_form.html', { + 'object_type': 'background task', + 'object': job_id, + 'form': form, + 'form_url': reverse('core:background_task_delete', kwargs={'job_id': job_id}) + }) + + def post(self, request, job_id): + form = ConfirmationForm(request.POST) + + if form.is_valid(): + # all the RQ queues should use the same connection + config = QUEUES_LIST[0] + try: + job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),) + except NoSuchJobError: + raise Http404(_("Job {job_id} not found").format(job_id=job_id)) + + queue_index = QUEUES_MAP[job.origin] + queue = get_queue_by_index(queue_index) + + # Remove job id from queue and delete the actual job + queue.connection.lrem(queue.key, 0, job.id) + job.delete() + messages.success(request, f'Deleted job {job_id}') + else: + messages.error(request, f'Error deleting job: {form.errors[0]}') + + return redirect(reverse('core:background_queue_list')) + + +class BackgroundTaskRequeueView(BaseRQView): + + def get(self, request, job_id): + # all the RQ queues should use the same connection + config = QUEUES_LIST[0] + try: + job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),) + except NoSuchJobError: + raise Http404(_("Job {job_id} not found").format(job_id=job_id)) + + queue_index = QUEUES_MAP[job.origin] + queue = get_queue_by_index(queue_index) + + requeue_job(job_id, connection=queue.connection, serializer=queue.serializer) + messages.success(request, f'You have successfully requeued: {job_id}') + return redirect(reverse('core:background_task', args=[job_id])) + + +class BackgroundTaskEnqueueView(BaseRQView): + + def get(self, request, job_id): + # all the RQ queues should use the same connection + config = QUEUES_LIST[0] + try: + job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),) + except NoSuchJobError: + raise Http404(_("Job {job_id} not found").format(job_id=job_id)) + + queue_index = QUEUES_MAP[job.origin] + queue = get_queue_by_index(queue_index) + + try: + # _enqueue_job is new in RQ 1.14, this is used to enqueue + # job regardless of its dependencies + queue._enqueue_job(job) + except AttributeError: + queue.enqueue_job(job) + + # Remove job from correct registry if needed + if job.get_status() == RQJobStatus.DEFERRED: + registry = DeferredJobRegistry(queue.name, queue.connection) + registry.remove(job) + elif job.get_status() == RQJobStatus.FINISHED: + registry = FinishedJobRegistry(queue.name, queue.connection) + registry.remove(job) + elif job.get_status() == RQJobStatus.SCHEDULED: + registry = ScheduledJobRegistry(queue.name, queue.connection) + registry.remove(job) + + messages.success(request, f'You have successfully enqueued: {job_id}') + return redirect(reverse('core:background_task', args=[job_id])) + + +class BackgroundTaskStopView(BaseRQView): + + def get(self, request, job_id): + # all the RQ queues should use the same connection + config = QUEUES_LIST[0] + try: + job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),) + except NoSuchJobError: + raise Http404(_("Job {job_id} not found").format(job_id=job_id)) + + queue_index = QUEUES_MAP[job.origin] + queue = get_queue_by_index(queue_index) + + stopped, _ = stop_jobs(queue, job_id) + if len(stopped) == 1: + messages.success(request, f'You have successfully stopped {job_id}') + else: + messages.error(request, f'Failed to stop {job_id}') + + return redirect(reverse('core:background_task', args=[job_id])) + + +class WorkerListView(TableMixin, BaseRQView): + table = tables.WorkerTable + + def get_table_data(self, request, queue): + clean_worker_registry(queue) + all_workers = Worker.all(queue.connection) + workers = [worker for worker in all_workers if queue.name in worker.queue_names()] + return workers + + def get(self, request, queue_index): + queue = get_queue_by_index(queue_index) + data = self.get_table_data(request, queue) + + table = self.get_table(data, request, False) + + # If this is an HTMX request, return only the rendered table HTML + if request.htmx: + if request.htmx.target != 'object_list': + table.embedded = True + # Hide selection checkboxes + if 'pk' in table.base_columns: + table.columns.hide('pk') + return render(request, 'htmx/table.html', { + 'table': table, + 'queue': queue, + }) + + return render(request, 'core/rq_worker_list.html', { + 'table': table, + 'queue': queue, + }) + + +class WorkerView(BaseRQView): + + def get(self, request, key): + # all the RQ queues should use the same connection + config = QUEUES_LIST[0] + worker = Worker.find_by_key('rq:worker:' + key, connection=get_redis_connection(config['connection_config'])) + # Convert microseconds to milliseconds + worker.total_working_time = worker.total_working_time / 1000 + + return render(request, 'core/rq_worker.html', { + 'worker': worker, + 'job': worker.get_current_job(), + 'total_working_time': worker.total_working_time * 1000, + }) + + +# +# Plugins +# + +class PluginListView(UserPassesTestMixin, View): + + def test_func(self): + return self.request.user.is_staff + + def get(self, request): + plugins = [ + # Look up app config by package name + apps.get_app_config(plugin.rsplit('.', 1)[-1]) for plugin in settings.PLUGINS + ] + table = tables.PluginTable(plugins, user=request.user) + table.configure(request) + + return render(request, 'core/plugin_list.html', { + 'plugins': plugins, + 'active_tab': 'api-tokens', + 'table': table, + }) diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py index 2a84a9a51..2939b986e 100644 --- a/netbox/dcim/forms/bulk_create.py +++ b/netbox/dcim/forms/bulk_create.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _ from dcim.models import * from extras.models import Tag from netbox.forms.mixins import CustomFieldsMixin -from utilities.forms import BootstrapMixin, form_from_model +from utilities.forms import form_from_model from utilities.forms.fields import DynamicModelMultipleChoiceField, ExpandableNameField from .object_create import ComponentCreateForm @@ -26,7 +26,7 @@ __all__ = ( # Device components # -class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm): +class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm): pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), widget=forms.MultipleHiddenInput() diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index d63873b59..f30ff91fa 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -727,7 +727,7 @@ class PowerOutletImportForm(NetBoxModelImportForm): help_text=_('Local power port which feeds this outlet') ) feed_leg = CSVChoiceField( - label=_('Feed lag'), + label=_('Feed leg'), choices=PowerOutletFeedLegChoices, required=False, help_text=_('Electrical phase (for three-phase circuits)') @@ -1359,6 +1359,10 @@ class VirtualDeviceContextImportForm(NetBoxModelImportForm): to_field_name='name', help_text='Assigned tenant' ) + status = CSVChoiceField( + label=_('Status'), + choices=VirtualDeviceContextStatusChoices, + ) class Meta: fields = [ diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index da3a2bea4..fdb5f1d5a 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -11,7 +11,7 @@ from extras.models import ConfigTemplate from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm -from utilities.forms import BootstrapMixin, add_blank_choice +from utilities.forms import add_blank_choice from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, @@ -748,7 +748,7 @@ class DeviceVCMembershipForm(forms.ModelForm): return vc_position -class VCMemberSelectForm(BootstrapMixin, forms.Form): +class VCMemberSelectForm(forms.Form): device = DynamicModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -771,7 +771,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form): # Device component templates # -class ComponentTemplateForm(BootstrapMixin, forms.ModelForm): +class ComponentTemplateForm(forms.ModelForm): device_type = DynamicModelChoiceField( label=_('Device type'), queryset=DeviceType.objects.all() @@ -1272,7 +1272,7 @@ class DeviceBayForm(DeviceComponentForm): ] -class PopulateDeviceBayForm(BootstrapMixin, forms.Form): +class PopulateDeviceBayForm(forms.Form): installed_device = forms.ModelChoiceField( queryset=Device.objects.all(), label=_('Child Device'), diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py index bab8876da..d46ef83ad 100644 --- a/netbox/dcim/forms/object_import.py +++ b/netbox/dcim/forms/object_import.py @@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _ from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices from dcim.models import * -from utilities.forms import BootstrapMixin from wireless.choices import WirelessRoleChoices __all__ = ( @@ -24,11 +23,7 @@ __all__ = ( # Component template import forms # -class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): - pass - - -class ConsolePortTemplateImportForm(ComponentTemplateImportForm): +class ConsolePortTemplateImportForm(forms.ModelForm): class Meta: model = ConsolePortTemplate @@ -37,7 +32,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm): ] -class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): +class ConsoleServerPortTemplateImportForm(forms.ModelForm): class Meta: model = ConsoleServerPortTemplate @@ -46,7 +41,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): ] -class PowerPortTemplateImportForm(ComponentTemplateImportForm): +class PowerPortTemplateImportForm(forms.ModelForm): class Meta: model = PowerPortTemplate @@ -55,7 +50,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm): ] -class PowerOutletTemplateImportForm(ComponentTemplateImportForm): +class PowerOutletTemplateImportForm(forms.ModelForm): power_port = forms.ModelChoiceField( label=_('Power port'), queryset=PowerPortTemplate.objects.all(), @@ -84,7 +79,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm): return module_type -class InterfaceTemplateImportForm(ComponentTemplateImportForm): +class InterfaceTemplateImportForm(forms.ModelForm): type = forms.ChoiceField( label=_('Type'), choices=InterfaceTypeChoices.CHOICES @@ -113,7 +108,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm): ] -class FrontPortTemplateImportForm(ComponentTemplateImportForm): +class FrontPortTemplateImportForm(forms.ModelForm): type = forms.ChoiceField( label=_('Type'), choices=PortTypeChoices.CHOICES @@ -145,7 +140,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm): ] -class RearPortTemplateImportForm(ComponentTemplateImportForm): +class RearPortTemplateImportForm(forms.ModelForm): type = forms.ChoiceField( label=_('Type'), choices=PortTypeChoices.CHOICES @@ -158,7 +153,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm): ] -class ModuleBayTemplateImportForm(ComponentTemplateImportForm): +class ModuleBayTemplateImportForm(forms.ModelForm): class Meta: model = ModuleBayTemplate @@ -167,7 +162,7 @@ class ModuleBayTemplateImportForm(ComponentTemplateImportForm): ] -class DeviceBayTemplateImportForm(ComponentTemplateImportForm): +class DeviceBayTemplateImportForm(forms.ModelForm): class Meta: model = DeviceBayTemplate @@ -176,7 +171,7 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm): ] -class InventoryItemTemplateImportForm(ComponentTemplateImportForm): +class InventoryItemTemplateImportForm(forms.ModelForm): parent = forms.ModelChoiceField( label=_('Parent'), queryset=InventoryItemTemplate.objects.all(), diff --git a/netbox/dcim/graphql/gfk_mixins.py b/netbox/dcim/graphql/gfk_mixins.py index c97aa4c2b..2f669fb87 100644 --- a/netbox/dcim/graphql/gfk_mixins.py +++ b/netbox/dcim/graphql/gfk_mixins.py @@ -1,6 +1,6 @@ import graphene -from circuits.graphql.types import CircuitTerminationType -from circuits.models import CircuitTermination +from circuits.graphql.types import CircuitTerminationType, ProviderNetworkType +from circuits.models import CircuitTermination, ProviderNetwork from dcim.graphql.types import ( ConsolePortTemplateType, ConsolePortType, @@ -167,3 +167,42 @@ class InventoryItemComponentType(graphene.Union): return PowerPortType if type(instance) is RearPort: return RearPortType + + +class ConnectedEndpointType(graphene.Union): + class Meta: + types = ( + CircuitTerminationType, + ConsolePortType, + ConsoleServerPortType, + FrontPortType, + InterfaceType, + PowerFeedType, + PowerOutletType, + PowerPortType, + ProviderNetworkType, + RearPortType, + ) + + @classmethod + def resolve_type(cls, instance, info): + if type(instance) is CircuitTermination: + return CircuitTerminationType + if type(instance) is ConsolePortType: + return ConsolePortType + if type(instance) is ConsoleServerPort: + return ConsoleServerPortType + if type(instance) is FrontPort: + return FrontPortType + if type(instance) is Interface: + return InterfaceType + if type(instance) is PowerFeed: + return PowerFeedType + if type(instance) is PowerOutlet: + return PowerOutletType + if type(instance) is PowerPort: + return PowerPortType + if type(instance) is ProviderNetwork: + return ProviderNetworkType + if type(instance) is RearPort: + return RearPortType diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py index f8e626fe8..8241b7de5 100644 --- a/netbox/dcim/graphql/mixins.py +++ b/netbox/dcim/graphql/mixins.py @@ -13,7 +13,7 @@ class CabledObjectMixin: class PathEndpointMixin: - connected_endpoints = graphene.List('dcim.graphql.gfk_mixins.LinkPeerType') + connected_endpoints = graphene.List('dcim.graphql.gfk_mixins.ConnectedEndpointType') def resolve_connected_endpoints(self, info): # Handle empty values diff --git a/netbox/dcim/management/commands/trace_paths.py b/netbox/dcim/management/commands/trace_paths.py index 4bb81bfd4..d34a428e4 100644 --- a/netbox/dcim/management/commands/trace_paths.py +++ b/netbox/dcim/management/commands/trace_paths.py @@ -34,7 +34,7 @@ class Command(BaseCommand): Draw a simple progress bar 20 increments wide illustrating the specified percentage. """ bar_size = int(percentage / 5) - self.stdout.write(f"\r [{'#' * bar_size}{' ' * (20-bar_size)}] {int(percentage)}%", ending='') + self.stdout.write(f"\r [{'#' * bar_size}{' ' * (20 - bar_size)}] {int(percentage)}%", ending='') def handle(self, *model_names, **options): diff --git a/netbox/dcim/migrations/0131_consoleport_speed.py b/netbox/dcim/migrations/0131_consoleport_speed.py deleted file mode 100644 index 350162218..000000000 --- a/netbox/dcim/migrations/0131_consoleport_speed.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0130_sitegroup'), - ] - - operations = [ - migrations.AlterField( - model_name='consoleport', - name='speed', - field=models.PositiveIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='consoleserverport', - name='speed', - field=models.PositiveIntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0131_squashed_0159.py b/netbox/dcim/migrations/0131_squashed_0159.py new file mode 100644 index 000000000..f7e7cfdb2 --- /dev/null +++ b/netbox/dcim/migrations/0131_squashed_0159.py @@ -0,0 +1,1194 @@ +import dcim.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields +import taggit.managers +import utilities.fields +import utilities.json +import utilities.ordering + + +class Migration(migrations.Migration): + + replaces = [ + ('dcim', '0131_consoleport_speed'), + ('dcim', '0132_cable_length'), + ('dcim', '0133_port_colors'), + ('dcim', '0134_interface_wwn_bridge'), + ('dcim', '0135_tenancy_extensions'), + ('dcim', '0136_device_airflow'), + ('dcim', '0137_relax_uniqueness_constraints'), + ('dcim', '0138_extend_tag_support'), + ('dcim', '0139_rename_cable_peer'), + ('dcim', '0140_wireless'), + ('dcim', '0141_asn_model'), + ('dcim', '0142_rename_128gfc_qsfp28'), + ('dcim', '0143_remove_primary_for_related_name'), + ('dcim', '0144_fix_cable_abs_length'), + ('dcim', '0145_site_remove_deprecated_fields'), + ('dcim', '0146_modules'), + ('dcim', '0147_inventoryitemrole'), + ('dcim', '0148_inventoryitem_component'), + ('dcim', '0149_inventoryitem_templates'), + ('dcim', '0150_interface_vrf'), + ('dcim', '0151_interface_speed_duplex'), + ('dcim', '0152_standardize_id_fields'), + ('dcim', '0153_created_datetimefield'), + ('dcim', '0154_half_height_rack_units'), + ('dcim', '0155_interface_poe_mode_type'), + ('dcim', '0156_location_status'), + ('dcim', '0157_new_cabling_models'), + ('dcim', '0158_populate_cable_terminations'), + ('dcim', '0159_populate_cable_paths') + ] + + dependencies = [ + ('tenancy', '0012_standardize_models'), + ('extras', '0002_squashed_0059'), + ('dcim', '0130_sitegroup'), + ('contenttypes', '0002_remove_content_type_name'), + ('ipam', '0053_asn_model'), + ('wireless', '0001_wireless'), + ] + + operations = [ + migrations.AlterField( + model_name='consoleport', + name='speed', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='consoleserverport', + name='speed', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='cable', + name='length', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + migrations.AddField( + model_name='frontport', + name='color', + field=utilities.fields.ColorField(blank=True, max_length=6), + ), + migrations.AddField( + model_name='frontporttemplate', + name='color', + field=utilities.fields.ColorField(blank=True, max_length=6), + ), + migrations.AddField( + model_name='rearport', + name='color', + field=utilities.fields.ColorField(blank=True, max_length=6), + ), + migrations.AddField( + model_name='rearporttemplate', + name='color', + field=utilities.fields.ColorField(blank=True, max_length=6), + ), + migrations.AddField( + model_name='interface', + name='wwn', + field=dcim.fields.WWNField(blank=True, null=True), + ), + migrations.AddField( + model_name='interface', + name='bridge', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bridge_interfaces', to='dcim.interface'), + ), + migrations.AddField( + model_name='location', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='locations', to='tenancy.tenant'), + ), + migrations.AddField( + model_name='cable', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='cables', to='tenancy.tenant'), + ), + migrations.AddField( + model_name='devicetype', + name='airflow', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='device', + name='airflow', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='region', + name='name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='region', + name='slug', + field=models.SlugField(max_length=100), + ), + migrations.AlterField( + model_name='sitegroup', + name='name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='sitegroup', + name='slug', + field=models.SlugField(max_length=100), + ), + migrations.AlterUniqueTogether( + name='location', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='location', + constraint=models.UniqueConstraint(fields=('site', 'parent', 'name'), name='dcim_location_parent_name'), + ), + migrations.AddConstraint( + model_name='location', + constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('site', 'name'), name='dcim_location_name'), + ), + migrations.AddConstraint( + model_name='location', + constraint=models.UniqueConstraint(fields=('site', 'parent', 'slug'), name='dcim_location_parent_slug'), + ), + migrations.AddConstraint( + model_name='location', + constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('site', 'slug'), name='dcim_location_slug'), + ), + migrations.AddConstraint( + model_name='region', + constraint=models.UniqueConstraint(fields=('parent', 'name'), name='dcim_region_parent_name'), + ), + migrations.AddConstraint( + model_name='region', + constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('name',), name='dcim_region_name'), + ), + migrations.AddConstraint( + model_name='region', + constraint=models.UniqueConstraint(fields=('parent', 'slug'), name='dcim_region_parent_slug'), + ), + migrations.AddConstraint( + model_name='region', + constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('slug',), name='dcim_region_slug'), + ), + migrations.AddConstraint( + model_name='sitegroup', + constraint=models.UniqueConstraint(fields=('parent', 'name'), name='dcim_sitegroup_parent_name'), + ), + migrations.AddConstraint( + model_name='sitegroup', + constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('name',), name='dcim_sitegroup_name'), + ), + migrations.AddConstraint( + model_name='sitegroup', + constraint=models.UniqueConstraint(fields=('parent', 'slug'), name='dcim_sitegroup_parent_slug'), + ), + migrations.AddConstraint( + model_name='sitegroup', + constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('slug',), name='dcim_sitegroup_slug'), + ), + migrations.AddField( + model_name='devicerole', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='location', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='manufacturer', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='platform', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='rackrole', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='region', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='sitegroup', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.RenameField( + model_name='consoleport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='consoleport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='consoleserverport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='consoleserverport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='frontport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='frontport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='interface', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='interface', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='powerfeed', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='powerfeed', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='poweroutlet', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='poweroutlet', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='powerport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='powerport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='rearport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='rearport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.AddField( + model_name='interface', + name='rf_role', + field=models.CharField(blank=True, max_length=30), + ), + migrations.AddField( + model_name='interface', + name='rf_channel', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='interface', + name='rf_channel_frequency', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True), + ), + migrations.AddField( + model_name='interface', + name='rf_channel_width', + field=models.DecimalField(blank=True, decimal_places=3, max_digits=7, null=True), + ), + migrations.AddField( + model_name='interface', + name='tx_power', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(127)]), + ), + migrations.AddField( + model_name='interface', + name='wireless_lans', + field=models.ManyToManyField(blank=True, related_name='interfaces', to='wireless.wirelesslan'), + ), + migrations.AddField( + model_name='interface', + name='wireless_link', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wireless.wirelesslink'), + ), + migrations.AddField( + model_name='site', + name='asns', + field=models.ManyToManyField(blank=True, related_name='sites', to='ipam.asn'), + ), + migrations.AlterField( + model_name='device', + name='primary_ip4', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'), + ), + migrations.AlterField( + model_name='device', + name='primary_ip6', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'), + ), + migrations.RemoveField( + model_name='site', + name='asn', + ), + migrations.RemoveField( + model_name='site', + name='contact_email', + ), + migrations.RemoveField( + model_name='site', + name='contact_name', + ), + migrations.RemoveField( + model_name='site', + name='contact_phone', + ), + migrations.RunSQL( + sql="\n DO $$\n DECLARE\n idx record;\n BEGIN\n FOR idx IN\n SELECT indexname AS old_name,\n replace(indexname, 'module', 'inventoryitem') AS new_name\n FROM pg_indexes\n WHERE schemaname = 'public' AND\n tablename = 'dcim_inventoryitem' AND\n indexname LIKE 'dcim_module_%'\n LOOP\n EXECUTE format(\n 'ALTER INDEX %I RENAME TO %I;',\n idx.old_name,\n idx.new_name\n );\n END LOOP;\n END$$;\n ", + ), + migrations.AlterModelOptions( + name='consoleporttemplate', + options={'ordering': ('device_type', 'module_type', '_name')}, + ), + migrations.AlterModelOptions( + name='consoleserverporttemplate', + options={'ordering': ('device_type', 'module_type', '_name')}, + ), + migrations.AlterModelOptions( + name='frontporttemplate', + options={'ordering': ('device_type', 'module_type', '_name')}, + ), + migrations.AlterModelOptions( + name='interfacetemplate', + options={'ordering': ('device_type', 'module_type', '_name')}, + ), + migrations.AlterModelOptions( + name='poweroutlettemplate', + options={'ordering': ('device_type', 'module_type', '_name')}, + ), + migrations.AlterModelOptions( + name='powerporttemplate', + options={'ordering': ('device_type', 'module_type', '_name')}, + ), + migrations.AlterModelOptions( + name='rearporttemplate', + options={'ordering': ('device_type', 'module_type', '_name')}, + ), + migrations.AlterField( + model_name='consoleporttemplate', + name='device_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), + ), + migrations.AlterField( + model_name='consoleserverporttemplate', + name='device_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), + ), + migrations.AlterField( + model_name='frontporttemplate', + name='device_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='device_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), + ), + migrations.AlterField( + model_name='poweroutlettemplate', + name='device_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), + ), + migrations.AlterField( + model_name='powerporttemplate', + name='device_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), + ), + migrations.AlterField( + model_name='rearporttemplate', + name='device_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), + ), + migrations.CreateModel( + name='ModuleType', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('model', models.CharField(max_length=100)), + ('part_number', models.CharField(blank=True, max_length=50)), + ('comments', models.TextField(blank=True)), + ('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='module_types', to='dcim.manufacturer')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ('manufacturer', 'model'), + 'unique_together': {('manufacturer', 'model')}, + }, + ), + migrations.CreateModel( + name='ModuleBay', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)), + ('label', models.CharField(blank=True, max_length=64)), + ('position', models.CharField(blank=True, max_length=30)), + ('description', models.CharField(blank=True, max_length=200)), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.device')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ('device', '_name'), + 'unique_together': {('device', 'name')}, + }, + ), + migrations.CreateModel( + name='Module', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('local_context_data', models.JSONField(blank=True, null=True)), + ('serial', models.CharField(blank=True, max_length=50)), + ('asset_tag', models.CharField(blank=True, max_length=50, null=True, unique=True)), + ('comments', models.TextField(blank=True)), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='dcim.device')), + ('module_bay', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='installed_module', to='dcim.modulebay')), + ('module_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='instances', to='dcim.moduletype')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ('module_bay',), + }, + ), + migrations.AddField( + model_name='consoleport', + name='module', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), + ), + migrations.AddField( + model_name='consoleporttemplate', + name='module_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), + ), + migrations.AddField( + model_name='consoleserverport', + name='module', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), + ), + migrations.AddField( + model_name='consoleserverporttemplate', + name='module_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), + ), + migrations.AddField( + model_name='frontport', + name='module', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), + ), + migrations.AddField( + model_name='frontporttemplate', + name='module_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), + ), + migrations.AddField( + model_name='interface', + name='module', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), + ), + migrations.AddField( + model_name='interfacetemplate', + name='module_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), + ), + migrations.AddField( + model_name='poweroutlet', + name='module', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='module_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), + ), + migrations.AddField( + model_name='powerport', + name='module', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), + ), + migrations.AddField( + model_name='powerporttemplate', + name='module_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), + ), + migrations.AddField( + model_name='rearport', + name='module', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), + ), + migrations.AddField( + model_name='rearporttemplate', + name='module_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), + ), + migrations.AlterUniqueTogether( + name='consoleporttemplate', + unique_together={('device_type', 'name'), ('module_type', 'name')}, + ), + migrations.AlterUniqueTogether( + name='consoleserverporttemplate', + unique_together={('device_type', 'name'), ('module_type', 'name')}, + ), + migrations.AlterUniqueTogether( + name='frontporttemplate', + unique_together={('rear_port', 'rear_port_position'), ('device_type', 'name'), ('module_type', 'name')}, + ), + migrations.AlterUniqueTogether( + name='interfacetemplate', + unique_together={('device_type', 'name'), ('module_type', 'name')}, + ), + migrations.AlterUniqueTogether( + name='poweroutlettemplate', + unique_together={('device_type', 'name'), ('module_type', 'name')}, + ), + migrations.AlterUniqueTogether( + name='powerporttemplate', + unique_together={('device_type', 'name'), ('module_type', 'name')}, + ), + migrations.AlterUniqueTogether( + name='rearporttemplate', + unique_together={('device_type', 'name'), ('module_type', 'name')}, + ), + migrations.CreateModel( + name='InventoryItemRole', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(max_length=100, unique=True)), + ('color', utilities.fields.ColorField(default='9e9e9e', max_length=6)), + ('description', models.CharField(blank=True, max_length=200)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.AddField( + model_name='inventoryitem', + name='role', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.inventoryitemrole'), + ), + migrations.AddField( + model_name='inventoryitem', + name='component_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='inventoryitem', + name='component_type', + field=models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='interface', + name='vrf', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces', to='ipam.vrf'), + ), + migrations.AddField( + model_name='interface', + name='duplex', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AddField( + model_name='interface', + name='speed', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='cable', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='cablepath', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='consoleport', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='consoleporttemplate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='consoleserverport', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='consoleserverporttemplate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='device', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='devicebay', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='devicebaytemplate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='devicerole', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='devicetype', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='frontport', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='frontporttemplate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='interface', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='inventoryitem', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='location', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='manufacturer', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='platform', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='powerfeed', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='poweroutlet', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='poweroutlettemplate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='powerpanel', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='powerport', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='powerporttemplate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='rack', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='rackreservation', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='rackrole', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='rearport', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='rearporttemplate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='region', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='site', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='sitegroup', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='virtualchassis', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='cable', + name='termination_a_id', + field=models.PositiveBigIntegerField(), + ), + migrations.AlterField( + model_name='cable', + name='termination_b_id', + field=models.PositiveBigIntegerField(), + ), + migrations.AlterField( + model_name='cablepath', + name='destination_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='cablepath', + name='origin_id', + field=models.PositiveBigIntegerField(), + ), + migrations.AlterField( + model_name='consoleport', + name='_link_peer_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='consoleserverport', + name='_link_peer_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='frontport', + name='_link_peer_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='interface', + name='_link_peer_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='powerfeed', + name='_link_peer_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='poweroutlet', + name='_link_peer_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='powerport', + name='_link_peer_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='rearport', + name='_link_peer_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='cable', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='consoleport', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='consoleporttemplate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='consoleserverport', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='consoleserverporttemplate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='device', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='devicebay', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='devicebaytemplate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='devicerole', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='devicetype', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='frontport', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='frontporttemplate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='interface', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='inventoryitem', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.CreateModel( + name='InventoryItemTemplate', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)), + ('label', models.CharField(blank=True, max_length=64)), + ('description', models.CharField(blank=True, max_length=200)), + ('component_id', models.PositiveBigIntegerField(blank=True, null=True)), + ('part_id', models.CharField(blank=True, max_length=50)), + ('lft', models.PositiveIntegerField(editable=False)), + ('rght', models.PositiveIntegerField(editable=False)), + ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), + ('level', models.PositiveIntegerField(editable=False)), + ('component_type', models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate', 'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), + ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype')), + ('manufacturer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_item_templates', to='dcim.manufacturer')), + ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.inventoryitemtemplate')), + ('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_item_templates', to='dcim.inventoryitemrole')), + ], + options={ + 'ordering': ('device_type__id', 'parent__id', '_name'), + 'unique_together': {('device_type', 'parent', 'name')}, + }, + ), + migrations.AlterField( + model_name='location', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='manufacturer', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.CreateModel( + name='ModuleBayTemplate', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)), + ('label', models.CharField(blank=True, max_length=64)), + ('position', models.CharField(blank=True, max_length=30)), + ('description', models.CharField(blank=True, max_length=200)), + ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype')), + ], + options={ + 'ordering': ('device_type', '_name'), + 'unique_together': {('device_type', 'name')}, + }, + ), + migrations.AlterField( + model_name='platform', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='powerfeed', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='poweroutlet', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='poweroutlettemplate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='powerpanel', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='powerport', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='powerporttemplate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='rack', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='rackreservation', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='rackrole', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='rearport', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='rearporttemplate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='region', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='site', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='sitegroup', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='virtualchassis', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='devicetype', + name='u_height', + field=models.DecimalField(decimal_places=1, default=1.0, max_digits=4), + ), + migrations.AlterField( + model_name='device', + name='position', + field=models.DecimalField(blank=True, decimal_places=1, max_digits=4, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100.5)]), + ), + migrations.AddField( + model_name='interface', + name='poe_mode', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='interface', + name='poe_type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='interfacetemplate', + name='poe_mode', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='interfacetemplate', + name='poe_type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='location', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.CreateModel( + name='CableTermination', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('cable_end', models.CharField(max_length=1)), + ('termination_id', models.PositiveBigIntegerField()), + ('cable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='dcim.cable')), + ('termination_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), + ('_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.device')), + ('_rack', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.rack')), + ('_location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location')), + ('_site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site')), + ], + options={ + 'ordering': ('cable', 'cable_end', 'pk'), + }, + ), + migrations.AddConstraint( + model_name='cabletermination', + constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='dcim_cable_termination_unique_termination'), + ), + migrations.RenameField( + model_name='cablepath', + old_name='path', + new_name='_nodes', + ), + migrations.AddField( + model_name='cablepath', + name='path', + field=models.JSONField(default=list), + ), + migrations.AddField( + model_name='cablepath', + name='is_complete', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='consoleport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='consoleserverport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='frontport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='interface', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='powerfeed', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='poweroutlet', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='powerport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='rearport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + ] diff --git a/netbox/dcim/migrations/0132_cable_length.py b/netbox/dcim/migrations/0132_cable_length.py deleted file mode 100644 index e20a8b8aa..000000000 --- a/netbox/dcim/migrations/0132_cable_length.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0131_consoleport_speed'), - ] - - operations = [ - migrations.AlterField( - model_name='cable', - name='length', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0133_port_colors.py b/netbox/dcim/migrations/0133_port_colors.py deleted file mode 100644 index 8cae7ac8e..000000000 --- a/netbox/dcim/migrations/0133_port_colors.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.db import migrations -import utilities.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0132_cable_length'), - ] - - operations = [ - migrations.AddField( - model_name='frontport', - name='color', - field=utilities.fields.ColorField(blank=True, max_length=6), - ), - migrations.AddField( - model_name='frontporttemplate', - name='color', - field=utilities.fields.ColorField(blank=True, max_length=6), - ), - migrations.AddField( - model_name='rearport', - name='color', - field=utilities.fields.ColorField(blank=True, max_length=6), - ), - migrations.AddField( - model_name='rearporttemplate', - name='color', - field=utilities.fields.ColorField(blank=True, max_length=6), - ), - ] diff --git a/netbox/dcim/migrations/0134_interface_wwn_bridge.py b/netbox/dcim/migrations/0134_interface_wwn_bridge.py deleted file mode 100644 index a900ae6be..000000000 --- a/netbox/dcim/migrations/0134_interface_wwn_bridge.py +++ /dev/null @@ -1,23 +0,0 @@ -import dcim.fields -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0133_port_colors'), - ] - - operations = [ - migrations.AddField( - model_name='interface', - name='wwn', - field=dcim.fields.WWNField(blank=True, null=True), - ), - migrations.AddField( - model_name='interface', - name='bridge', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bridge_interfaces', to='dcim.interface'), - ), - ] diff --git a/netbox/dcim/migrations/0135_tenancy_extensions.py b/netbox/dcim/migrations/0135_tenancy_extensions.py deleted file mode 100644 index 96d765eea..000000000 --- a/netbox/dcim/migrations/0135_tenancy_extensions.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('tenancy', '0002_tenant_ordering'), - ('dcim', '0134_interface_wwn_bridge'), - ] - - operations = [ - migrations.AddField( - model_name='location', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='locations', to='tenancy.tenant'), - ), - migrations.AddField( - model_name='cable', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='cables', to='tenancy.tenant'), - ), - ] diff --git a/netbox/dcim/migrations/0136_device_airflow.py b/netbox/dcim/migrations/0136_device_airflow.py deleted file mode 100644 index 94cc89f3f..000000000 --- a/netbox/dcim/migrations/0136_device_airflow.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0135_tenancy_extensions'), - ] - - operations = [ - migrations.AddField( - model_name='devicetype', - name='airflow', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='device', - name='airflow', - field=models.CharField(blank=True, max_length=50), - ), - ] diff --git a/netbox/dcim/migrations/0137_relax_uniqueness_constraints.py b/netbox/dcim/migrations/0137_relax_uniqueness_constraints.py deleted file mode 100644 index 7cedb1b08..000000000 --- a/netbox/dcim/migrations/0137_relax_uniqueness_constraints.py +++ /dev/null @@ -1,83 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0136_device_airflow'), - ] - - operations = [ - migrations.AlterField( - model_name='region', - name='name', - field=models.CharField(max_length=100), - ), - migrations.AlterField( - model_name='region', - name='slug', - field=models.SlugField(max_length=100), - ), - migrations.AlterField( - model_name='sitegroup', - name='name', - field=models.CharField(max_length=100), - ), - migrations.AlterField( - model_name='sitegroup', - name='slug', - field=models.SlugField(max_length=100), - ), - migrations.AlterUniqueTogether( - name='location', - unique_together=set(), - ), - migrations.AddConstraint( - model_name='location', - constraint=models.UniqueConstraint(fields=('site', 'parent', 'name'), name='dcim_location_parent_name'), - ), - migrations.AddConstraint( - model_name='location', - constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('site', 'name'), name='dcim_location_name'), - ), - migrations.AddConstraint( - model_name='location', - constraint=models.UniqueConstraint(fields=('site', 'parent', 'slug'), name='dcim_location_parent_slug'), - ), - migrations.AddConstraint( - model_name='location', - constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('site', 'slug'), name='dcim_location_slug'), - ), - migrations.AddConstraint( - model_name='region', - constraint=models.UniqueConstraint(fields=('parent', 'name'), name='dcim_region_parent_name'), - ), - migrations.AddConstraint( - model_name='region', - constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('name',), name='dcim_region_name'), - ), - migrations.AddConstraint( - model_name='region', - constraint=models.UniqueConstraint(fields=('parent', 'slug'), name='dcim_region_parent_slug'), - ), - migrations.AddConstraint( - model_name='region', - constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('slug',), name='dcim_region_slug'), - ), - migrations.AddConstraint( - model_name='sitegroup', - constraint=models.UniqueConstraint(fields=('parent', 'name'), name='dcim_sitegroup_parent_name'), - ), - migrations.AddConstraint( - model_name='sitegroup', - constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('name',), name='dcim_sitegroup_name'), - ), - migrations.AddConstraint( - model_name='sitegroup', - constraint=models.UniqueConstraint(fields=('parent', 'slug'), name='dcim_sitegroup_parent_slug'), - ), - migrations.AddConstraint( - model_name='sitegroup', - constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('slug',), name='dcim_sitegroup_slug'), - ), - ] diff --git a/netbox/dcim/migrations/0138_extend_tag_support.py b/netbox/dcim/migrations/0138_extend_tag_support.py deleted file mode 100644 index 763b53c50..000000000 --- a/netbox/dcim/migrations/0138_extend_tag_support.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-21 14:50 - -from django.db import migrations -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0062_clear_secrets_changelog'), - ('dcim', '0137_relax_uniqueness_constraints'), - ] - - operations = [ - migrations.AddField( - model_name='devicerole', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - migrations.AddField( - model_name='location', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - migrations.AddField( - model_name='manufacturer', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - migrations.AddField( - model_name='platform', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - migrations.AddField( - model_name='rackrole', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - migrations.AddField( - model_name='region', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - migrations.AddField( - model_name='sitegroup', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - ] diff --git a/netbox/dcim/migrations/0139_rename_cable_peer.py b/netbox/dcim/migrations/0139_rename_cable_peer.py deleted file mode 100644 index 59dc04e2a..000000000 --- a/netbox/dcim/migrations/0139_rename_cable_peer.py +++ /dev/null @@ -1,91 +0,0 @@ -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0138_extend_tag_support'), - ] - - operations = [ - migrations.RenameField( - model_name='consoleport', - old_name='_cable_peer_id', - new_name='_link_peer_id', - ), - migrations.RenameField( - model_name='consoleport', - old_name='_cable_peer_type', - new_name='_link_peer_type', - ), - migrations.RenameField( - model_name='consoleserverport', - old_name='_cable_peer_id', - new_name='_link_peer_id', - ), - migrations.RenameField( - model_name='consoleserverport', - old_name='_cable_peer_type', - new_name='_link_peer_type', - ), - migrations.RenameField( - model_name='frontport', - old_name='_cable_peer_id', - new_name='_link_peer_id', - ), - migrations.RenameField( - model_name='frontport', - old_name='_cable_peer_type', - new_name='_link_peer_type', - ), - migrations.RenameField( - model_name='interface', - old_name='_cable_peer_id', - new_name='_link_peer_id', - ), - migrations.RenameField( - model_name='interface', - old_name='_cable_peer_type', - new_name='_link_peer_type', - ), - migrations.RenameField( - model_name='powerfeed', - old_name='_cable_peer_id', - new_name='_link_peer_id', - ), - migrations.RenameField( - model_name='powerfeed', - old_name='_cable_peer_type', - new_name='_link_peer_type', - ), - migrations.RenameField( - model_name='poweroutlet', - old_name='_cable_peer_id', - new_name='_link_peer_id', - ), - migrations.RenameField( - model_name='poweroutlet', - old_name='_cable_peer_type', - new_name='_link_peer_type', - ), - migrations.RenameField( - model_name='powerport', - old_name='_cable_peer_id', - new_name='_link_peer_id', - ), - migrations.RenameField( - model_name='powerport', - old_name='_cable_peer_type', - new_name='_link_peer_type', - ), - migrations.RenameField( - model_name='rearport', - old_name='_cable_peer_id', - new_name='_link_peer_id', - ), - migrations.RenameField( - model_name='rearport', - old_name='_cable_peer_type', - new_name='_link_peer_type', - ), - ] diff --git a/netbox/dcim/migrations/0140_wireless.py b/netbox/dcim/migrations/0140_wireless.py deleted file mode 100644 index 430782cf0..000000000 --- a/netbox/dcim/migrations/0140_wireless.py +++ /dev/null @@ -1,49 +0,0 @@ -from django.db import migrations, models -import django.core.validators -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0139_rename_cable_peer'), - ('wireless', '0001_wireless'), - ] - - operations = [ - migrations.AddField( - model_name='interface', - name='rf_role', - field=models.CharField(blank=True, max_length=30), - ), - migrations.AddField( - model_name='interface', - name='rf_channel', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='interface', - name='rf_channel_frequency', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True), - ), - migrations.AddField( - model_name='interface', - name='rf_channel_width', - field=models.DecimalField(blank=True, decimal_places=3, max_digits=7, null=True), - ), - migrations.AddField( - model_name='interface', - name='tx_power', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(127)]), - ), - migrations.AddField( - model_name='interface', - name='wireless_lans', - field=models.ManyToManyField(blank=True, related_name='interfaces', to='wireless.WirelessLAN'), - ), - migrations.AddField( - model_name='interface', - name='wireless_link', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wireless.wirelesslink'), - ), - ] diff --git a/netbox/dcim/migrations/0141_asn_model.py b/netbox/dcim/migrations/0141_asn_model.py deleted file mode 100644 index 6f011f35d..000000000 --- a/netbox/dcim/migrations/0141_asn_model.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.8 on 2021-11-02 16:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0053_asn_model'), - ('dcim', '0140_wireless'), - ] - - operations = [ - migrations.AddField( - model_name='site', - name='asns', - field=models.ManyToManyField(blank=True, related_name='sites', to='ipam.ASN'), - ), - ] diff --git a/netbox/dcim/migrations/0142_rename_128gfc_qsfp28.py b/netbox/dcim/migrations/0142_rename_128gfc_qsfp28.py deleted file mode 100644 index d1f91afae..000000000 --- a/netbox/dcim/migrations/0142_rename_128gfc_qsfp28.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.db import migrations - -OLD_VALUE = '128gfc-sfp28' -NEW_VALUE = '128gfc-qsfp28' - - -def correct_type(apps, schema_editor): - """ - Correct TYPE_128GFC_QSFP28 interface type. - """ - Interface = apps.get_model('dcim', 'Interface') - InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate') - - for model in (Interface, InterfaceTemplate): - model.objects.filter(type=OLD_VALUE).update(type=NEW_VALUE) - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0141_asn_model'), - ] - - operations = [ - migrations.RunPython( - code=correct_type, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/migrations/0143_remove_primary_for_related_name.py b/netbox/dcim/migrations/0143_remove_primary_for_related_name.py deleted file mode 100644 index 820c9e3fe..000000000 --- a/netbox/dcim/migrations/0143_remove_primary_for_related_name.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0053_asn_model'), - ('dcim', '0142_rename_128gfc_qsfp28'), - ] - - operations = [ - migrations.AlterField( - model_name='device', - name='primary_ip4', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'), - ), - migrations.AlterField( - model_name='device', - name='primary_ip6', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'), - ), - ] diff --git a/netbox/dcim/migrations/0144_fix_cable_abs_length.py b/netbox/dcim/migrations/0144_fix_cable_abs_length.py deleted file mode 100644 index 0da30ffb5..000000000 --- a/netbox/dcim/migrations/0144_fix_cable_abs_length.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import migrations - -from utilities.utils import to_meters - - -def recalculate_abs_length(apps, schema_editor): - """ - Recalculate absolute lengths for all cables with a length and length unit defined. Fixes - incorrectly calculated values as reported under bug #8377. - """ - Cable = apps.get_model('dcim', 'Cable') - - cables = Cable.objects.filter(length__isnull=False).exclude(length_unit='') - for cable in cables: - cable._abs_length = to_meters(cable.length, cable.length_unit) - - Cable.objects.bulk_update(cables, ['_abs_length'], batch_size=100) - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0143_remove_primary_for_related_name'), - ] - - operations = [ - migrations.RunPython( - code=recalculate_abs_length, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/migrations/0145_site_remove_deprecated_fields.py b/netbox/dcim/migrations/0145_site_remove_deprecated_fields.py deleted file mode 100644 index 86918447d..000000000 --- a/netbox/dcim/migrations/0145_site_remove_deprecated_fields.py +++ /dev/null @@ -1,59 +0,0 @@ -import os - -from django.db import migrations -from django.db.utils import DataError - - -def check_legacy_data(apps, schema_editor): - """ - Abort the migration if any legacy site fields still contain data. - """ - Site = apps.get_model('dcim', 'Site') - - site_count = Site.objects.exclude(asn__isnull=True).count() - if site_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ: - raise DataError( - f"Unable to proceed with deleting asn field from Site model: Found {site_count} sites with " - f"legacy ASN data. Please ensure all legacy site ASN data has been migrated to ASN objects " - f"before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA environment variable to bypass " - f"this safeguard and delete all legacy site ASN data." - ) - - site_count = Site.objects.exclude(contact_name='', contact_phone='', contact_email='').count() - if site_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ: - raise DataError( - f"Unable to proceed with deleting contact fields from Site model: Found {site_count} sites " - f"with legacy contact data. Please ensure all legacy site contact data has been migrated to " - f"contact objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA environment " - f"variable to bypass this safeguard and delete all legacy site contact data." - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0144_fix_cable_abs_length'), - ] - - operations = [ - migrations.RunPython( - code=check_legacy_data, - reverse_code=migrations.RunPython.noop - ), - migrations.RemoveField( - model_name='site', - name='asn', - ), - migrations.RemoveField( - model_name='site', - name='contact_email', - ), - migrations.RemoveField( - model_name='site', - name='contact_name', - ), - migrations.RemoveField( - model_name='site', - name='contact_phone', - ), - ] diff --git a/netbox/dcim/migrations/0146_modules.py b/netbox/dcim/migrations/0146_modules.py deleted file mode 100644 index 821cf6119..000000000 --- a/netbox/dcim/migrations/0146_modules.py +++ /dev/null @@ -1,279 +0,0 @@ -from utilities.json import CustomFieldJSONEncoder -from django.db import migrations, models -import django.db.models.deletion -import taggit.managers -import utilities.fields -import utilities.ordering - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0066_customfield_name_validation'), - ('dcim', '0145_site_remove_deprecated_fields'), - ] - - operations = [ - # Rename any indexes left over from the old Module model (now InventoryItem) (#8656) - migrations.RunSQL( - """ - DO $$ - DECLARE - idx record; - BEGIN - FOR idx IN - SELECT indexname AS old_name, - replace(indexname, 'module', 'inventoryitem') AS new_name - FROM pg_indexes - WHERE schemaname = 'public' AND - tablename = 'dcim_inventoryitem' AND - indexname LIKE 'dcim_module_%' - LOOP - EXECUTE format( - 'ALTER INDEX %I RENAME TO %I;', - idx.old_name, - idx.new_name - ); - END LOOP; - END$$; - """ - ), - - migrations.AlterModelOptions( - name='consoleporttemplate', - options={'ordering': ('device_type', 'module_type', '_name')}, - ), - migrations.AlterModelOptions( - name='consoleserverporttemplate', - options={'ordering': ('device_type', 'module_type', '_name')}, - ), - migrations.AlterModelOptions( - name='frontporttemplate', - options={'ordering': ('device_type', 'module_type', '_name')}, - ), - migrations.AlterModelOptions( - name='interfacetemplate', - options={'ordering': ('device_type', 'module_type', '_name')}, - ), - migrations.AlterModelOptions( - name='poweroutlettemplate', - options={'ordering': ('device_type', 'module_type', '_name')}, - ), - migrations.AlterModelOptions( - name='powerporttemplate', - options={'ordering': ('device_type', 'module_type', '_name')}, - ), - migrations.AlterModelOptions( - name='rearporttemplate', - options={'ordering': ('device_type', 'module_type', '_name')}, - ), - migrations.AlterField( - model_name='consoleporttemplate', - name='device_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), - ), - migrations.AlterField( - model_name='consoleserverporttemplate', - name='device_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), - ), - migrations.AlterField( - model_name='frontporttemplate', - name='device_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='device_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), - ), - migrations.AlterField( - model_name='poweroutlettemplate', - name='device_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), - ), - migrations.AlterField( - model_name='powerporttemplate', - name='device_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), - ), - migrations.AlterField( - model_name='rearporttemplate', - name='device_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'), - ), - migrations.CreateModel( - name='ModuleType', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('model', models.CharField(max_length=100)), - ('part_number', models.CharField(blank=True, max_length=50)), - ('comments', models.TextField(blank=True)), - ('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='module_types', to='dcim.manufacturer')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ], - options={ - 'ordering': ('manufacturer', 'model'), - 'unique_together': {('manufacturer', 'model')}, - }, - ), - migrations.CreateModel( - name='ModuleBay', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=64)), - ('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)), - ('label', models.CharField(blank=True, max_length=64)), - ('position', models.CharField(blank=True, max_length=30)), - ('description', models.CharField(blank=True, max_length=200)), - ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.device')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ], - options={ - 'ordering': ('device', '_name'), - 'unique_together': {('device', 'name')}, - }, - ), - migrations.CreateModel( - name='Module', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('local_context_data', models.JSONField(blank=True, null=True)), - ('serial', models.CharField(blank=True, max_length=50)), - ('asset_tag', models.CharField(blank=True, max_length=50, null=True, unique=True)), - ('comments', models.TextField(blank=True)), - ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='dcim.device')), - ('module_bay', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='installed_module', to='dcim.modulebay')), - ('module_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='instances', to='dcim.moduletype')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ], - options={ - 'ordering': ('module_bay',), - }, - ), - migrations.AddField( - model_name='consoleport', - name='module', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), - ), - migrations.AddField( - model_name='consoleporttemplate', - name='module_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), - ), - migrations.AddField( - model_name='consoleserverport', - name='module', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), - ), - migrations.AddField( - model_name='consoleserverporttemplate', - name='module_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), - ), - migrations.AddField( - model_name='frontport', - name='module', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), - ), - migrations.AddField( - model_name='frontporttemplate', - name='module_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), - ), - migrations.AddField( - model_name='interface', - name='module', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), - ), - migrations.AddField( - model_name='interfacetemplate', - name='module_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), - ), - migrations.AddField( - model_name='poweroutlet', - name='module', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), - ), - migrations.AddField( - model_name='poweroutlettemplate', - name='module_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), - ), - migrations.AddField( - model_name='powerport', - name='module', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), - ), - migrations.AddField( - model_name='powerporttemplate', - name='module_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), - ), - migrations.AddField( - model_name='rearport', - name='module', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'), - ), - migrations.AddField( - model_name='rearporttemplate', - name='module_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'), - ), - migrations.AlterUniqueTogether( - name='consoleporttemplate', - unique_together={('device_type', 'name'), ('module_type', 'name')}, - ), - migrations.AlterUniqueTogether( - name='consoleserverporttemplate', - unique_together={('device_type', 'name'), ('module_type', 'name')}, - ), - migrations.AlterUniqueTogether( - name='frontporttemplate', - unique_together={('device_type', 'name'), ('rear_port', 'rear_port_position'), ('module_type', 'name')}, - ), - migrations.AlterUniqueTogether( - name='interfacetemplate', - unique_together={('device_type', 'name'), ('module_type', 'name')}, - ), - migrations.AlterUniqueTogether( - name='poweroutlettemplate', - unique_together={('device_type', 'name'), ('module_type', 'name')}, - ), - migrations.AlterUniqueTogether( - name='powerporttemplate', - unique_together={('device_type', 'name'), ('module_type', 'name')}, - ), - migrations.AlterUniqueTogether( - name='rearporttemplate', - unique_together={('device_type', 'name'), ('module_type', 'name')}, - ), - migrations.CreateModel( - name='ModuleBayTemplate', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=64)), - ('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)), - ('label', models.CharField(blank=True, max_length=64)), - ('position', models.CharField(blank=True, max_length=30)), - ('description', models.CharField(blank=True, max_length=200)), - ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype')), - ], - options={ - 'ordering': ('device_type', '_name'), - 'unique_together': {('device_type', 'name')}, - }, - ), - ] diff --git a/netbox/dcim/migrations/0147_inventoryitemrole.py b/netbox/dcim/migrations/0147_inventoryitemrole.py deleted file mode 100644 index 4b6c27450..000000000 --- a/netbox/dcim/migrations/0147_inventoryitemrole.py +++ /dev/null @@ -1,38 +0,0 @@ -from utilities.json import CustomFieldJSONEncoder -from django.db import migrations, models -import django.db.models.deletion -import taggit.managers -import utilities.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0068_configcontext_cluster_types'), - ('dcim', '0146_modules'), - ] - - operations = [ - migrations.CreateModel( - name='InventoryItemRole', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, unique=True)), - ('slug', models.SlugField(max_length=100, unique=True)), - ('color', utilities.fields.ColorField(default='9e9e9e', max_length=6)), - ('description', models.CharField(blank=True, max_length=200)), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ], - options={ - 'ordering': ('name',), - }, - ), - migrations.AddField( - model_name='inventoryitem', - name='role', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.inventoryitemrole'), - ), - ] diff --git a/netbox/dcim/migrations/0148_inventoryitem_component.py b/netbox/dcim/migrations/0148_inventoryitem_component.py deleted file mode 100644 index a18f41d3d..000000000 --- a/netbox/dcim/migrations/0148_inventoryitem_component.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('dcim', '0147_inventoryitemrole'), - ] - - operations = [ - migrations.AddField( - model_name='inventoryitem', - name='component_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name='inventoryitem', - name='component_type', - field=models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'), - ), - ] diff --git a/netbox/dcim/migrations/0149_inventoryitem_templates.py b/netbox/dcim/migrations/0149_inventoryitem_templates.py deleted file mode 100644 index f0b1f3cff..000000000 --- a/netbox/dcim/migrations/0149_inventoryitem_templates.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion -import mptt.fields -import utilities.fields -import utilities.ordering - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('dcim', '0148_inventoryitem_component'), - ] - - operations = [ - migrations.CreateModel( - name='InventoryItemTemplate', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=64)), - ('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)), - ('label', models.CharField(blank=True, max_length=64)), - ('description', models.CharField(blank=True, max_length=200)), - ('component_id', models.PositiveBigIntegerField(blank=True, null=True)), - ('part_id', models.CharField(blank=True, max_length=50)), - ('lft', models.PositiveIntegerField(editable=False)), - ('rght', models.PositiveIntegerField(editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(editable=False)), - ('component_type', models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate', 'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), - ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype')), - ('manufacturer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_item_templates', to='dcim.manufacturer')), - ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.inventoryitemtemplate')), - ('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_item_templates', to='dcim.inventoryitemrole')), - ], - options={ - 'ordering': ('device_type__id', 'parent__id', '_name'), - 'unique_together': {('device_type', 'parent', 'name')}, - }, - ), - ] diff --git a/netbox/dcim/migrations/0150_interface_vrf.py b/netbox/dcim/migrations/0150_interface_vrf.py deleted file mode 100644 index f8741e4a0..000000000 --- a/netbox/dcim/migrations/0150_interface_vrf.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.11 on 2022-01-07 18:34 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0054_vlangroup_min_max_vids'), - ('dcim', '0149_inventoryitem_templates'), - ] - - operations = [ - migrations.AddField( - model_name='interface', - name='vrf', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces', to='ipam.vrf'), - ), - ] diff --git a/netbox/dcim/migrations/0151_interface_speed_duplex.py b/netbox/dcim/migrations/0151_interface_speed_duplex.py deleted file mode 100644 index 7e800f42a..000000000 --- a/netbox/dcim/migrations/0151_interface_speed_duplex.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.10 on 2022-01-08 18:23 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0150_interface_vrf'), - ] - - operations = [ - migrations.AddField( - model_name='interface', - name='duplex', - field=models.CharField(blank=True, max_length=50, null=True), - ), - migrations.AddField( - model_name='interface', - name='speed', - field=models.PositiveIntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0152_standardize_id_fields.py b/netbox/dcim/migrations/0152_standardize_id_fields.py deleted file mode 100644 index 6bf5b43f4..000000000 --- a/netbox/dcim/migrations/0152_standardize_id_fields.py +++ /dev/null @@ -1,274 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0151_interface_speed_duplex'), - ] - - operations = [ - # Model IDs - migrations.AlterField( - model_name='cable', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='cablepath', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='consoleport', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='consoleporttemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='consoleserverport', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='consoleserverporttemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='device', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='devicebay', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='devicebaytemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='devicerole', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='devicetype', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='frontport', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='frontporttemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='interface', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='inventoryitem', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='inventoryitemrole', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='inventoryitemtemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='location', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='manufacturer', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='module', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='modulebay', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='modulebaytemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='moduletype', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='platform', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='powerfeed', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='poweroutlet', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='poweroutlettemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='powerpanel', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='powerport', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='powerporttemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='rack', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='rackreservation', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='rackrole', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='rearport', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='rearporttemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='region', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='site', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='sitegroup', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='virtualchassis', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - - # GFK IDs - migrations.AlterField( - model_name='cable', - name='termination_a_id', - field=models.PositiveBigIntegerField(), - ), - migrations.AlterField( - model_name='cable', - name='termination_b_id', - field=models.PositiveBigIntegerField(), - ), - migrations.AlterField( - model_name='cablepath', - name='destination_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='cablepath', - name='origin_id', - field=models.PositiveBigIntegerField(), - ), - migrations.AlterField( - model_name='consoleport', - name='_link_peer_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='consoleserverport', - name='_link_peer_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='frontport', - name='_link_peer_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='interface', - name='_link_peer_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='powerfeed', - name='_link_peer_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='poweroutlet', - name='_link_peer_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='powerport', - name='_link_peer_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='rearport', - name='_link_peer_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0153_created_datetimefield.py b/netbox/dcim/migrations/0153_created_datetimefield.py deleted file mode 100644 index c1cc4132e..000000000 --- a/netbox/dcim/migrations/0153_created_datetimefield.py +++ /dev/null @@ -1,208 +0,0 @@ -# Generated by Django 4.0.2 on 2022-02-08 18:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0152_standardize_id_fields'), - ] - - operations = [ - migrations.AlterField( - model_name='cable', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='consoleport', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='consoleporttemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='consoleserverport', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='consoleserverporttemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='device', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='devicebay', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='devicebaytemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='devicerole', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='devicetype', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='frontport', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='frontporttemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='interface', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='inventoryitem', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='inventoryitemrole', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='inventoryitemtemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='location', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='manufacturer', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='module', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='modulebay', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='modulebaytemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='moduletype', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='platform', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='powerfeed', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='poweroutlet', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='poweroutlettemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='powerpanel', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='powerport', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='powerporttemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='rack', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='rackreservation', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='rackrole', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='rearport', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='rearporttemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='region', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='site', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='sitegroup', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='virtualchassis', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0154_half_height_rack_units.py b/netbox/dcim/migrations/0154_half_height_rack_units.py deleted file mode 100644 index f212aa21a..000000000 --- a/netbox/dcim/migrations/0154_half_height_rack_units.py +++ /dev/null @@ -1,23 +0,0 @@ -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0153_created_datetimefield'), - ] - - operations = [ - migrations.AlterField( - model_name='devicetype', - name='u_height', - field=models.DecimalField(decimal_places=1, default=1.0, max_digits=4), - ), - migrations.AlterField( - model_name='device', - name='position', - field=models.DecimalField(blank=True, decimal_places=1, max_digits=4, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100.5)]), - ), - ] diff --git a/netbox/dcim/migrations/0155_interface_poe_mode_type.py b/netbox/dcim/migrations/0155_interface_poe_mode_type.py deleted file mode 100644 index 13f2ddfc0..000000000 --- a/netbox/dcim/migrations/0155_interface_poe_mode_type.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.0.5 on 2022-06-22 00:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0154_half_height_rack_units'), - ] - - operations = [ - migrations.AddField( - model_name='interface', - name='poe_mode', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='interface', - name='poe_type', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='interfacetemplate', - name='poe_mode', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='interfacetemplate', - name='poe_type', - field=models.CharField(blank=True, max_length=50), - ), - ] diff --git a/netbox/dcim/migrations/0156_location_status.py b/netbox/dcim/migrations/0156_location_status.py deleted file mode 100644 index b20273755..000000000 --- a/netbox/dcim/migrations/0156_location_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.5 on 2022-06-22 17:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0155_interface_poe_mode_type'), - ] - - operations = [ - migrations.AddField( - model_name='location', - name='status', - field=models.CharField(default='active', max_length=50), - ), - ] diff --git a/netbox/dcim/migrations/0157_new_cabling_models.py b/netbox/dcim/migrations/0157_new_cabling_models.py deleted file mode 100644 index a3a650086..000000000 --- a/netbox/dcim/migrations/0157_new_cabling_models.py +++ /dev/null @@ -1,95 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('dcim', '0156_location_status'), - ] - - operations = [ - - # Create CableTermination model - migrations.CreateModel( - name='CableTermination', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('cable_end', models.CharField(max_length=1)), - ('termination_id', models.PositiveBigIntegerField()), - ('cable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='dcim.cable')), - ('termination_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), - ('_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.device')), - ('_rack', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.rack')), - ('_location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location')), - ('_site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site')), - ], - options={ - 'ordering': ('cable', 'cable_end', 'pk'), - }, - ), - migrations.AddConstraint( - model_name='cabletermination', - constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='dcim_cable_termination_unique_termination'), - ), - - # Update CablePath model - migrations.RenameField( - model_name='cablepath', - old_name='path', - new_name='_nodes', - ), - migrations.AddField( - model_name='cablepath', - name='path', - field=models.JSONField(default=list), - ), - migrations.AddField( - model_name='cablepath', - name='is_complete', - field=models.BooleanField(default=False), - ), - - # Add cable_end field to cable termination models - migrations.AddField( - model_name='consoleport', - name='cable_end', - field=models.CharField(blank=True, max_length=1), - ), - migrations.AddField( - model_name='consoleserverport', - name='cable_end', - field=models.CharField(blank=True, max_length=1), - ), - migrations.AddField( - model_name='frontport', - name='cable_end', - field=models.CharField(blank=True, max_length=1), - ), - migrations.AddField( - model_name='interface', - name='cable_end', - field=models.CharField(blank=True, max_length=1), - ), - migrations.AddField( - model_name='powerfeed', - name='cable_end', - field=models.CharField(blank=True, max_length=1), - ), - migrations.AddField( - model_name='poweroutlet', - name='cable_end', - field=models.CharField(blank=True, max_length=1), - ), - migrations.AddField( - model_name='powerport', - name='cable_end', - field=models.CharField(blank=True, max_length=1), - ), - migrations.AddField( - model_name='rearport', - name='cable_end', - field=models.CharField(blank=True, max_length=1), - ), - ] diff --git a/netbox/dcim/migrations/0158_populate_cable_terminations.py b/netbox/dcim/migrations/0158_populate_cable_terminations.py deleted file mode 100644 index 72d7f154a..000000000 --- a/netbox/dcim/migrations/0158_populate_cable_terminations.py +++ /dev/null @@ -1,87 +0,0 @@ -import sys - -from django.db import migrations - - -def cache_related_objects(termination): - """ - Replicate caching logic from CableTermination.cache_related_objects() - """ - attrs = {} - - # Device components - if getattr(termination, 'device', None): - attrs['_device'] = termination.device - attrs['_rack'] = termination.device.rack - attrs['_location'] = termination.device.location - attrs['_site'] = termination.device.site - - # Power feeds - elif getattr(termination, 'rack', None): - attrs['_rack'] = termination.rack - attrs['_location'] = termination.rack.location - attrs['_site'] = termination.rack.site - - # Circuit terminations - elif getattr(termination, 'site', None): - attrs['_site'] = termination.site - - return attrs - - -def populate_cable_terminations(apps, schema_editor): - """ - Replicate terminations from the Cable model into CableTermination instances. - """ - ContentType = apps.get_model('contenttypes', 'ContentType') - Cable = apps.get_model('dcim', 'Cable') - CableTermination = apps.get_model('dcim', 'CableTermination') - - # Retrieve the necessary data from Cable objects - cables = Cable.objects.values( - 'id', 'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id' - ) - - # Queue CableTerminations to be created - cable_terminations = [] - cable_count = cables.count() - for i, cable in enumerate(cables, start=1): - for cable_end in ('a', 'b'): - # We must manually instantiate the termination object, because GFK fields are not - # supported within migrations. - termination_ct = ContentType.objects.get(pk=cable[f'termination_{cable_end}_type']) - termination_model = apps.get_model(termination_ct.app_label, termination_ct.model) - termination = termination_model.objects.get(pk=cable[f'termination_{cable_end}_id']) - - cable_terminations.append(CableTermination( - cable_id=cable['id'], - cable_end=cable_end.upper(), - termination_type_id=cable[f'termination_{cable_end}_type'], - termination_id=cable[f'termination_{cable_end}_id'], - **cache_related_objects(termination) - )) - - # Output progress occasionally - if 'test' not in sys.argv and not i % 100: - progress = float(i) * 100 / cable_count - if i == 100: - print('') - sys.stdout.write(f"\r Updated {i}/{cable_count} cables ({progress:.2f}%)") - sys.stdout.flush() - - # Bulk create the termination objects - CableTermination.objects.bulk_create(cable_terminations, batch_size=100) - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0157_new_cabling_models'), - ] - - operations = [ - migrations.RunPython( - code=populate_cable_terminations, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/migrations/0159_populate_cable_paths.py b/netbox/dcim/migrations/0159_populate_cable_paths.py deleted file mode 100644 index 22fe4b67e..000000000 --- a/netbox/dcim/migrations/0159_populate_cable_paths.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.db import migrations - -from dcim.utils import compile_path_node - - -def populate_cable_paths(apps, schema_editor): - """ - Replicate terminations from the Cable model into CableTermination instances. - """ - CablePath = apps.get_model('dcim', 'CablePath') - - # Construct the new two-dimensional path, and add the origin & destination objects to the nodes list - cable_paths = [] - for cablepath in CablePath.objects.all(): - - # Origin - origin = compile_path_node(cablepath.origin_type_id, cablepath.origin_id) - cablepath.path.append([origin]) - cablepath._nodes.insert(0, origin) - - # Transit nodes - cablepath.path.extend([ - [node] for node in cablepath._nodes[1:] - ]) - - # Destination - if cablepath.destination_id: - destination = compile_path_node(cablepath.destination_type_id, cablepath.destination_id) - cablepath.path.append([destination]) - cablepath._nodes.append(destination) - cablepath.is_complete = True - - cable_paths.append(cablepath) - - # Bulk update all CableTerminations - CablePath.objects.bulk_update(cable_paths, fields=('path', '_nodes', 'is_complete'), batch_size=100) - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0158_populate_cable_terminations'), - ] - - operations = [ - migrations.RunPython( - code=populate_cable_paths, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/migrations/0160_populate_cable_ends.py b/netbox/dcim/migrations/0160_populate_cable_ends.py deleted file mode 100644 index 53e042abc..000000000 --- a/netbox/dcim/migrations/0160_populate_cable_ends.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.db import migrations - - -def populate_cable_terminations(apps, schema_editor): - Cable = apps.get_model('dcim', 'Cable') - - cable_termination_models = ( - apps.get_model('dcim', 'ConsolePort'), - apps.get_model('dcim', 'ConsoleServerPort'), - apps.get_model('dcim', 'PowerPort'), - apps.get_model('dcim', 'PowerOutlet'), - apps.get_model('dcim', 'Interface'), - apps.get_model('dcim', 'FrontPort'), - apps.get_model('dcim', 'RearPort'), - apps.get_model('dcim', 'PowerFeed'), - apps.get_model('circuits', 'CircuitTermination'), - ) - - for model in cable_termination_models: - model.objects.filter( - id__in=Cable.objects.filter( - termination_a_type__app_label=model._meta.app_label, - termination_a_type__model=model._meta.model_name - ).values_list('termination_a_id', flat=True) - ).update(cable_end='A') - model.objects.filter( - id__in=Cable.objects.filter( - termination_b_type__app_label=model._meta.app_label, - termination_b_type__model=model._meta.model_name - ).values_list('termination_b_id', flat=True) - ).update(cable_end='B') - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0037_new_cabling_models'), - ('dcim', '0159_populate_cable_paths'), - ] - - operations = [ - migrations.RunPython( - code=populate_cable_terminations, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/migrations/0162_unique_constraints.py b/netbox/dcim/migrations/0160_squashed_0166.py similarity index 58% rename from netbox/dcim/migrations/0162_unique_constraints.py rename to netbox/dcim/migrations/0160_squashed_0166.py index d52dbb6c9..440a8115e 100644 --- a/netbox/dcim/migrations/0162_unique_constraints.py +++ b/netbox/dcim/migrations/0160_squashed_0166.py @@ -1,14 +1,146 @@ -from django.db import migrations, models import django.db.models.functions.text +import taggit.managers +from django.db import migrations, models + +import utilities.json class Migration(migrations.Migration): - dependencies = [ + replaces = [ + ('dcim', '0160_populate_cable_ends'), ('dcim', '0161_cabling_cleanup'), + ('dcim', '0162_unique_constraints'), + ('dcim', '0163_weight_fields'), + ('dcim', '0164_rack_mounting_depth'), + ('dcim', '0165_standardize_description_comments'), + ('dcim', '0166_virtualdevicecontext') + ] + + dependencies = [ + ('ipam', '0047_squashed_0053'), + ('tenancy', '0009_standardize_description_comments'), + ('circuits', '0037_new_cabling_models'), + ('dcim', '0159_populate_cable_paths'), ] operations = [ + migrations.AlterModelOptions( + name='cable', + options={'ordering': ('pk',)}, + ), + migrations.AlterUniqueTogether( + name='cable', + unique_together=set(), + ), + migrations.RemoveField( + model_name='cable', + name='termination_a_id', + ), + migrations.RemoveField( + model_name='cable', + name='termination_a_type', + ), + migrations.RemoveField( + model_name='cable', + name='termination_b_id', + ), + migrations.RemoveField( + model_name='cable', + name='termination_b_type', + ), + migrations.RemoveField( + model_name='cable', + name='_termination_a_device', + ), + migrations.RemoveField( + model_name='cable', + name='_termination_b_device', + ), + migrations.AlterUniqueTogether( + name='cablepath', + unique_together=set(), + ), + migrations.RemoveField( + model_name='cablepath', + name='destination_id', + ), + migrations.RemoveField( + model_name='cablepath', + name='destination_type', + ), + migrations.RemoveField( + model_name='cablepath', + name='origin_id', + ), + migrations.RemoveField( + model_name='cablepath', + name='origin_type', + ), + migrations.RemoveField( + model_name='consoleport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='consoleport', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='consoleserverport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='consoleserverport', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='frontport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='frontport', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='interface', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='interface', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='powerfeed', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='powerfeed', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='poweroutlet', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='poweroutlet', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='powerport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='powerport', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='rearport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='rearport', + name='_link_peer_type', + ), migrations.RemoveConstraint( model_name='cabletermination', name='dcim_cable_termination_unique_termination', @@ -329,4 +461,164 @@ class Migration(migrations.Migration): model_name='sitegroup', constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('slug',), name='dcim_sitegroup_slug', violation_error_message='A top-level site group with this slug already exists.'), ), + migrations.AddField( + model_name='devicetype', + name='weight', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + migrations.AddField( + model_name='devicetype', + name='weight_unit', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='devicetype', + name='_abs_weight', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='moduletype', + name='weight', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + migrations.AddField( + model_name='moduletype', + name='weight_unit', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='moduletype', + name='_abs_weight', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rack', + name='weight', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + migrations.AddField( + model_name='rack', + name='max_weight', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rack', + name='weight_unit', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='rack', + name='_abs_weight', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rack', + name='_abs_max_weight', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rack', + name='mounting_depth', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='cable', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='cable', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='device', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='devicetype', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='module', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='moduletype', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='powerfeed', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='powerpanel', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='powerpanel', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='rack', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='rackreservation', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='virtualchassis', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='virtualchassis', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.CreateModel( + name='VirtualDeviceContext', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('description', models.CharField(blank=True, max_length=200)), + ('name', models.CharField(max_length=64)), + ('status', models.CharField(max_length=50)), + ('identifier', models.PositiveSmallIntegerField(blank=True, null=True)), + ('comments', models.TextField(blank=True)), + ('device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vdcs', to='dcim.device')), + ('primary_ip4', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress')), + ('primary_ip6', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vdcs', to='tenancy.tenant')), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='interface', + name='vdcs', + field=models.ManyToManyField(related_name='interfaces', to='dcim.virtualdevicecontext'), + ), + migrations.AddConstraint( + model_name='virtualdevicecontext', + constraint=models.UniqueConstraint(fields=('device', 'identifier'), name='dcim_virtualdevicecontext_device_identifier'), + ), + migrations.AddConstraint( + model_name='virtualdevicecontext', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_virtualdevicecontext_device_name'), + ), ] diff --git a/netbox/dcim/migrations/0161_cabling_cleanup.py b/netbox/dcim/migrations/0161_cabling_cleanup.py deleted file mode 100644 index 8a1b7a09e..000000000 --- a/netbox/dcim/migrations/0161_cabling_cleanup.py +++ /dev/null @@ -1,134 +0,0 @@ -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0160_populate_cable_ends'), - ] - - operations = [ - - # Remove old fields from Cable - migrations.AlterModelOptions( - name='cable', - options={'ordering': ('pk',)}, - ), - migrations.AlterUniqueTogether( - name='cable', - unique_together=set(), - ), - migrations.RemoveField( - model_name='cable', - name='termination_a_id', - ), - migrations.RemoveField( - model_name='cable', - name='termination_a_type', - ), - migrations.RemoveField( - model_name='cable', - name='termination_b_id', - ), - migrations.RemoveField( - model_name='cable', - name='termination_b_type', - ), - migrations.RemoveField( - model_name='cable', - name='_termination_a_device', - ), - migrations.RemoveField( - model_name='cable', - name='_termination_b_device', - ), - - # Remove old fields from CablePath - migrations.AlterUniqueTogether( - name='cablepath', - unique_together=set(), - ), - migrations.RemoveField( - model_name='cablepath', - name='destination_id', - ), - migrations.RemoveField( - model_name='cablepath', - name='destination_type', - ), - migrations.RemoveField( - model_name='cablepath', - name='origin_id', - ), - migrations.RemoveField( - model_name='cablepath', - name='origin_type', - ), - - # Remove link peer type/ID fields from cable termination models - migrations.RemoveField( - model_name='consoleport', - name='_link_peer_id', - ), - migrations.RemoveField( - model_name='consoleport', - name='_link_peer_type', - ), - migrations.RemoveField( - model_name='consoleserverport', - name='_link_peer_id', - ), - migrations.RemoveField( - model_name='consoleserverport', - name='_link_peer_type', - ), - migrations.RemoveField( - model_name='frontport', - name='_link_peer_id', - ), - migrations.RemoveField( - model_name='frontport', - name='_link_peer_type', - ), - migrations.RemoveField( - model_name='interface', - name='_link_peer_id', - ), - migrations.RemoveField( - model_name='interface', - name='_link_peer_type', - ), - migrations.RemoveField( - model_name='powerfeed', - name='_link_peer_id', - ), - migrations.RemoveField( - model_name='powerfeed', - name='_link_peer_type', - ), - migrations.RemoveField( - model_name='poweroutlet', - name='_link_peer_id', - ), - migrations.RemoveField( - model_name='poweroutlet', - name='_link_peer_type', - ), - migrations.RemoveField( - model_name='powerport', - name='_link_peer_id', - ), - migrations.RemoveField( - model_name='powerport', - name='_link_peer_type', - ), - migrations.RemoveField( - model_name='rearport', - name='_link_peer_id', - ), - migrations.RemoveField( - model_name='rearport', - name='_link_peer_type', - ), - - ] diff --git a/netbox/dcim/migrations/0163_weight_fields.py b/netbox/dcim/migrations/0163_weight_fields.py deleted file mode 100644 index ddcc01164..000000000 --- a/netbox/dcim/migrations/0163_weight_fields.py +++ /dev/null @@ -1,72 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0162_unique_constraints'), - ] - - operations = [ - - # Device types - migrations.AddField( - model_name='devicetype', - name='weight', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), - ), - migrations.AddField( - model_name='devicetype', - name='weight_unit', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='devicetype', - name='_abs_weight', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - - # Module types - migrations.AddField( - model_name='moduletype', - name='weight', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), - ), - migrations.AddField( - model_name='moduletype', - name='weight_unit', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='moduletype', - name='_abs_weight', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - - # Racks - migrations.AddField( - model_name='rack', - name='weight', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), - ), - migrations.AddField( - model_name='rack', - name='max_weight', - field=models.PositiveIntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name='rack', - name='weight_unit', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='rack', - name='_abs_weight', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name='rack', - name='_abs_max_weight', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0164_rack_mounting_depth.py b/netbox/dcim/migrations/0164_rack_mounting_depth.py deleted file mode 100644 index 96836e0d6..000000000 --- a/netbox/dcim/migrations/0164_rack_mounting_depth.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.1 on 2022-10-27 14:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0163_weight_fields'), - ] - - operations = [ - migrations.AddField( - model_name='rack', - name='mounting_depth', - field=models.PositiveSmallIntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0165_standardize_description_comments.py b/netbox/dcim/migrations/0165_standardize_description_comments.py deleted file mode 100644 index f17f1d321..000000000 --- a/netbox/dcim/migrations/0165_standardize_description_comments.py +++ /dev/null @@ -1,78 +0,0 @@ -# Generated by Django 4.1.2 on 2022-11-03 18:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0164_rack_mounting_depth'), - ] - - operations = [ - migrations.AddField( - model_name='cable', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='cable', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - migrations.AddField( - model_name='device', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - migrations.AddField( - model_name='devicetype', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - migrations.AddField( - model_name='module', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - migrations.AddField( - model_name='moduletype', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - migrations.AddField( - model_name='powerfeed', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - migrations.AddField( - model_name='powerpanel', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='powerpanel', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - migrations.AddField( - model_name='rack', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - migrations.AddField( - model_name='rackreservation', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='virtualchassis', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='virtualchassis', - name='description', - field=models.CharField(blank=True, max_length=200), - ), - ] diff --git a/netbox/dcim/migrations/0166_virtualdevicecontext.py b/netbox/dcim/migrations/0166_virtualdevicecontext.py deleted file mode 100644 index 05becbdc6..000000000 --- a/netbox/dcim/migrations/0166_virtualdevicecontext.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 4.1.2 on 2022-11-10 16:56 - -from django.db import migrations, models -import django.db.models.deletion -import taggit.managers -import utilities.json - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0063_standardize_description_comments'), - ('extras', '0082_savedfilter'), - ('tenancy', '0009_standardize_description_comments'), - ('dcim', '0165_standardize_description_comments'), - ] - - operations = [ - migrations.CreateModel( - name='VirtualDeviceContext', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), - ('description', models.CharField(blank=True, max_length=200)), - ('name', models.CharField(max_length=64)), - ('status', models.CharField(max_length=50)), - ('identifier', models.PositiveSmallIntegerField(blank=True, null=True)), - ('comments', models.TextField(blank=True)), - ('device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vdcs', to='dcim.device')), - ('primary_ip4', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress')), - ('primary_ip6', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vdcs', to='tenancy.tenant')), - ], - options={ - 'ordering': ['name'], - }, - ), - migrations.AddField( - model_name='interface', - name='vdcs', - field=models.ManyToManyField(related_name='interfaces', to='dcim.virtualdevicecontext'), - ), - migrations.AddConstraint( - model_name='virtualdevicecontext', - constraint=models.UniqueConstraint(fields=('device', 'identifier'), name='dcim_virtualdevicecontext_device_identifier'), - ), - migrations.AddConstraint( - model_name='virtualdevicecontext', - constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_virtualdevicecontext_device_name'), - ), - ] diff --git a/netbox/dcim/migrations/0167_module_status.py b/netbox/dcim/migrations/0167_module_status.py deleted file mode 100644 index c048b4bd8..000000000 --- a/netbox/dcim/migrations/0167_module_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.2 on 2022-12-09 15:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0166_virtualdevicecontext'), - ] - - operations = [ - migrations.AddField( - model_name='module', - name='status', - field=models.CharField(default='active', max_length=50), - ), - ] diff --git a/netbox/dcim/migrations/0167_squashed_0182.py b/netbox/dcim/migrations/0167_squashed_0182.py new file mode 100644 index 000000000..735cb3efa --- /dev/null +++ b/netbox/dcim/migrations/0167_squashed_0182.py @@ -0,0 +1,251 @@ +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + +import utilities.fields + + +class Migration(migrations.Migration): + + replaces = [ + ('dcim', '0167_module_status'), + ('dcim', '0168_interface_template_enabled'), + ('dcim', '0169_devicetype_default_platform'), + ('dcim', '0170_configtemplate'), + ('dcim', '0171_cabletermination_change_logging'), + ('dcim', '0172_larger_power_draw_values'), + ('dcim', '0173_remove_napalm_fields'), + ('dcim', '0174_device_latitude_device_longitude'), + ('dcim', '0174_rack_starting_unit'), + ('dcim', '0175_device_oob_ip'), + ('dcim', '0176_device_component_counters'), + ('dcim', '0177_devicetype_component_counters'), + ('dcim', '0178_virtual_chassis_member_counter'), + ('dcim', '0179_interfacetemplate_rf_role'), + ('dcim', '0180_powerfeed_tenant'), + ('dcim', '0181_rename_device_role_device_role'), + ('dcim', '0182_zero_length_cable_fix') + ] + + dependencies = [ + ('extras', '0086_configtemplate'), + ('tenancy', '0010_tenant_relax_uniqueness'), + ('ipam', '0047_squashed_0053'), + ('dcim', '0166_virtualdevicecontext'), + ] + + operations = [ + migrations.AddField( + model_name='module', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.AddField( + model_name='interfacetemplate', + name='enabled', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='interfacetemplate', + name='bridge', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bridge_interfaces', to='dcim.interfacetemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='default_platform', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.platform'), + ), + migrations.AddField( + model_name='device', + name='config_template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss', to='extras.configtemplate'), + ), + migrations.AddField( + model_name='devicerole', + name='config_template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='device_roles', to='extras.configtemplate'), + ), + migrations.AddField( + model_name='platform', + name='config_template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='platforms', to='extras.configtemplate'), + ), + migrations.AddField( + model_name='cabletermination', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='cabletermination', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AlterField( + model_name='powerport', + name='allocated_draw', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AlterField( + model_name='powerport', + name='maximum_draw', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AlterField( + model_name='powerporttemplate', + name='allocated_draw', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AlterField( + model_name='powerporttemplate', + name='maximum_draw', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.RemoveField( + model_name='platform', + name='napalm_args', + ), + migrations.RemoveField( + model_name='platform', + name='napalm_driver', + ), + migrations.AddField( + model_name='device', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True), + ), + migrations.AddField( + model_name='device', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='rack', + name='starting_unit', + field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='device', + name='oob_ip', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'), + ), + migrations.AddField( + model_name='device', + name='console_port_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.ConsolePort'), + ), + migrations.AddField( + model_name='device', + name='console_server_port_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.ConsoleServerPort'), + ), + migrations.AddField( + model_name='device', + name='power_port_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.PowerPort'), + ), + migrations.AddField( + model_name='device', + name='power_outlet_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.PowerOutlet'), + ), + migrations.AddField( + model_name='device', + name='interface_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.Interface'), + ), + migrations.AddField( + model_name='device', + name='front_port_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.FrontPort'), + ), + migrations.AddField( + model_name='device', + name='rear_port_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.RearPort'), + ), + migrations.AddField( + model_name='device', + name='device_bay_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.DeviceBay'), + ), + migrations.AddField( + model_name='device', + name='module_bay_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.ModuleBay'), + ), + migrations.AddField( + model_name='device', + name='inventory_item_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device', to_model='dcim.InventoryItem'), + ), + migrations.AddField( + model_name='devicetype', + name='console_port_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.ConsolePortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='console_server_port_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.ConsoleServerPortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='power_port_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.PowerPortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='power_outlet_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.PowerOutletTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='interface_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.InterfaceTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='front_port_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.FrontPortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='rear_port_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.RearPortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='device_bay_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.DeviceBayTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='module_bay_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.ModuleBayTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='inventory_item_template_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='device_type', to_model='dcim.InventoryItemTemplate'), + ), + migrations.AddField( + model_name='virtualchassis', + name='member_count', + field=utilities.fields.CounterCacheField(default=0, editable=False, to_field='virtual_chassis', to_model='dcim.Device'), + ), + migrations.AddField( + model_name='interfacetemplate', + name='rf_role', + field=models.CharField(blank=True, max_length=30), + ), + migrations.AddField( + model_name='powerfeed', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='power_feeds', to='tenancy.tenant'), + ), + migrations.RenameField( + model_name='device', + old_name='device_role', + new_name='role', + ), + ] diff --git a/netbox/dcim/migrations/0168_interface_template_enabled.py b/netbox/dcim/migrations/0168_interface_template_enabled.py deleted file mode 100644 index af18be51d..000000000 --- a/netbox/dcim/migrations/0168_interface_template_enabled.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0167_module_status'), - ] - - operations = [ - migrations.AddField( - model_name='interfacetemplate', - name='enabled', - field=models.BooleanField(default=True), - ), - migrations.AddField( - model_name='interfacetemplate', - name='bridge', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bridge_interfaces', to='dcim.interfacetemplate'), - ), - ] diff --git a/netbox/dcim/migrations/0169_devicetype_default_platform.py b/netbox/dcim/migrations/0169_devicetype_default_platform.py deleted file mode 100644 index a143f2c62..000000000 --- a/netbox/dcim/migrations/0169_devicetype_default_platform.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.1.6 on 2023-02-10 18:06 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0168_interface_template_enabled'), - ] - - operations = [ - migrations.AddField( - model_name='devicetype', - name='default_platform', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.platform'), - ), - ] diff --git a/netbox/dcim/migrations/0170_configtemplate.py b/netbox/dcim/migrations/0170_configtemplate.py deleted file mode 100644 index f9508424d..000000000 --- a/netbox/dcim/migrations/0170_configtemplate.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0086_configtemplate'), - ('dcim', '0169_devicetype_default_platform'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='config_template', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss', to='extras.configtemplate'), - ), - migrations.AddField( - model_name='devicerole', - name='config_template', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='device_roles', to='extras.configtemplate'), - ), - migrations.AddField( - model_name='platform', - name='config_template', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='platforms', to='extras.configtemplate'), - ), - ] diff --git a/netbox/dcim/migrations/0171_cabletermination_change_logging.py b/netbox/dcim/migrations/0171_cabletermination_change_logging.py deleted file mode 100644 index e2131e45d..000000000 --- a/netbox/dcim/migrations/0171_cabletermination_change_logging.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0170_configtemplate'), - ] - - operations = [ - migrations.AddField( - model_name='cabletermination', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='cabletermination', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0172_larger_power_draw_values.py b/netbox/dcim/migrations/0172_larger_power_draw_values.py deleted file mode 100644 index 729daf836..000000000 --- a/netbox/dcim/migrations/0172_larger_power_draw_values.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 4.1.9 on 2023-05-12 18:46 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0171_cabletermination_change_logging'), - ] - - operations = [ - migrations.AlterField( - model_name='powerport', - name='allocated_draw', - field=models.PositiveIntegerField( - blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)] - ), - ), - migrations.AlterField( - model_name='powerport', - name='maximum_draw', - field=models.PositiveIntegerField( - blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)] - ), - ), - migrations.AlterField( - model_name='powerporttemplate', - name='allocated_draw', - field=models.PositiveIntegerField( - blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)] - ), - ), - migrations.AlterField( - model_name='powerporttemplate', - name='maximum_draw', - field=models.PositiveIntegerField( - blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)] - ), - ), - ] diff --git a/netbox/dcim/migrations/0173_remove_napalm_fields.py b/netbox/dcim/migrations/0173_remove_napalm_fields.py deleted file mode 100644 index 61c7c5695..000000000 --- a/netbox/dcim/migrations/0173_remove_napalm_fields.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0172_larger_power_draw_values'), - ] - - operations = [ - migrations.RemoveField( - model_name='platform', - name='napalm_args', - ), - migrations.RemoveField( - model_name='platform', - name='napalm_driver', - ), - ] diff --git a/netbox/dcim/migrations/0174_device_latitude_device_longitude.py b/netbox/dcim/migrations/0174_device_latitude_device_longitude.py deleted file mode 100644 index f9f72f9f8..000000000 --- a/netbox/dcim/migrations/0174_device_latitude_device_longitude.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.1.9 on 2023-05-31 22:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('dcim', '0173_remove_napalm_fields'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='latitude', - field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True), - ), - migrations.AddField( - model_name='device', - name='longitude', - field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0174_rack_starting_unit.py b/netbox/dcim/migrations/0174_rack_starting_unit.py deleted file mode 100644 index 2d2b5f826..000000000 --- a/netbox/dcim/migrations/0174_rack_starting_unit.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.9 on 2023-05-31 15:47 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('dcim', '0174_device_latitude_device_longitude'), - ] - - operations = [ - migrations.AddField( - model_name='rack', - name='starting_unit', - field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)]), - ), - ] diff --git a/netbox/dcim/migrations/0175_device_oob_ip.py b/netbox/dcim/migrations/0175_device_oob_ip.py deleted file mode 100644 index bf6a88ba8..000000000 --- a/netbox/dcim/migrations/0175_device_oob_ip.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.1.9 on 2023-07-24 20:29 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - dependencies = [ - ('ipam', '0066_iprange_mark_utilized'), - ('dcim', '0174_rack_starting_unit'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='oob_ip', - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name='+', - to='ipam.ipaddress', - ), - ), - ] diff --git a/netbox/dcim/migrations/0176_device_component_counters.py b/netbox/dcim/migrations/0176_device_component_counters.py deleted file mode 100644 index 60857ecb9..000000000 --- a/netbox/dcim/migrations/0176_device_component_counters.py +++ /dev/null @@ -1,83 +0,0 @@ -from django.db import migrations -from django.db.models import Count - -import utilities.fields -from utilities.counters import update_counts - - -def recalculate_device_counts(apps, schema_editor): - Device = apps.get_model("dcim", "Device") - - update_counts(Device, 'console_port_count', 'consoleports') - update_counts(Device, 'console_server_port_count', 'consoleserverports') - update_counts(Device, 'power_port_count', 'powerports') - update_counts(Device, 'power_outlet_count', 'poweroutlets') - update_counts(Device, 'interface_count', 'interfaces') - update_counts(Device, 'front_port_count', 'frontports') - update_counts(Device, 'rear_port_count', 'rearports') - update_counts(Device, 'device_bay_count', 'devicebays') - update_counts(Device, 'module_bay_count', 'modulebays') - update_counts(Device, 'inventory_item_count', 'inventoryitems') - - -class Migration(migrations.Migration): - dependencies = [ - ('dcim', '0175_device_oob_ip'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='console_port_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.ConsolePort'), - ), - migrations.AddField( - model_name='device', - name='console_server_port_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.ConsoleServerPort'), - ), - migrations.AddField( - model_name='device', - name='power_port_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.PowerPort'), - ), - migrations.AddField( - model_name='device', - name='power_outlet_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.PowerOutlet'), - ), - migrations.AddField( - model_name='device', - name='interface_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.Interface'), - ), - migrations.AddField( - model_name='device', - name='front_port_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.FrontPort'), - ), - migrations.AddField( - model_name='device', - name='rear_port_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.RearPort'), - ), - migrations.AddField( - model_name='device', - name='device_bay_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.DeviceBay'), - ), - migrations.AddField( - model_name='device', - name='module_bay_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.ModuleBay'), - ), - migrations.AddField( - model_name='device', - name='inventory_item_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.InventoryItem'), - ), - migrations.RunPython( - recalculate_device_counts, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/migrations/0177_devicetype_component_counters.py b/netbox/dcim/migrations/0177_devicetype_component_counters.py deleted file mode 100644 index b452ce2d9..000000000 --- a/netbox/dcim/migrations/0177_devicetype_component_counters.py +++ /dev/null @@ -1,83 +0,0 @@ -from django.db import migrations -from django.db.models import Count - -import utilities.fields -from utilities.counters import update_counts - - -def recalculate_devicetype_template_counts(apps, schema_editor): - DeviceType = apps.get_model("dcim", "DeviceType") - - update_counts(DeviceType, 'console_port_template_count', 'consoleporttemplates') - update_counts(DeviceType, 'console_server_port_template_count', 'consoleserverporttemplates') - update_counts(DeviceType, 'power_port_template_count', 'powerporttemplates') - update_counts(DeviceType, 'power_outlet_template_count', 'poweroutlettemplates') - update_counts(DeviceType, 'interface_template_count', 'interfacetemplates') - update_counts(DeviceType, 'front_port_template_count', 'frontporttemplates') - update_counts(DeviceType, 'rear_port_template_count', 'rearporttemplates') - update_counts(DeviceType, 'device_bay_template_count', 'devicebaytemplates') - update_counts(DeviceType, 'module_bay_template_count', 'modulebaytemplates') - update_counts(DeviceType, 'inventory_item_template_count', 'inventoryitemtemplates') - - -class Migration(migrations.Migration): - dependencies = [ - ('dcim', '0176_device_component_counters'), - ] - - operations = [ - migrations.AddField( - model_name='devicetype', - name='console_port_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.ConsolePortTemplate'), - ), - migrations.AddField( - model_name='devicetype', - name='console_server_port_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.ConsoleServerPortTemplate'), - ), - migrations.AddField( - model_name='devicetype', - name='power_port_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.PowerPortTemplate'), - ), - migrations.AddField( - model_name='devicetype', - name='power_outlet_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.PowerOutletTemplate'), - ), - migrations.AddField( - model_name='devicetype', - name='interface_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.InterfaceTemplate'), - ), - migrations.AddField( - model_name='devicetype', - name='front_port_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.FrontPortTemplate'), - ), - migrations.AddField( - model_name='devicetype', - name='rear_port_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.RearPortTemplate'), - ), - migrations.AddField( - model_name='devicetype', - name='device_bay_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.DeviceBayTemplate'), - ), - migrations.AddField( - model_name='devicetype', - name='module_bay_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.ModuleBayTemplate'), - ), - migrations.AddField( - model_name='devicetype', - name='inventory_item_template_count', - field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.InventoryItemTemplate'), - ), - migrations.RunPython( - recalculate_devicetype_template_counts, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py b/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py deleted file mode 100644 index 99b304b66..000000000 --- a/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import migrations -from django.db.models import Count - -import utilities.fields -from utilities.counters import update_counts - - -def populate_virtualchassis_members(apps, schema_editor): - VirtualChassis = apps.get_model('dcim', 'VirtualChassis') - - update_counts(VirtualChassis, 'member_count', 'members') - - -class Migration(migrations.Migration): - dependencies = [ - ('dcim', '0177_devicetype_component_counters'), - ] - - operations = [ - migrations.AddField( - model_name='virtualchassis', - name='member_count', - field=utilities.fields.CounterCacheField( - default=0, to_field='virtual_chassis', to_model='dcim.Device' - ), - ), - migrations.RunPython( - code=populate_virtualchassis_members, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/migrations/0179_interfacetemplate_rf_role.py b/netbox/dcim/migrations/0179_interfacetemplate_rf_role.py deleted file mode 100644 index 44eb08853..000000000 --- a/netbox/dcim/migrations/0179_interfacetemplate_rf_role.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.2 on 2023-07-18 07:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0178_virtual_chassis_member_counter'), - ] - - operations = [ - migrations.AddField( - model_name='interfacetemplate', - name='rf_role', - field=models.CharField(blank=True, max_length=30), - ), - ] diff --git a/netbox/dcim/migrations/0180_powerfeed_tenant.py b/netbox/dcim/migrations/0180_powerfeed_tenant.py deleted file mode 100644 index af550b21d..000000000 --- a/netbox/dcim/migrations/0180_powerfeed_tenant.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.1.8 on 2023-07-29 11:29 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('tenancy', '0010_tenant_relax_uniqueness'), - ('dcim', '0179_interfacetemplate_rf_role'), - ] - - operations = [ - migrations.AddField( - model_name='powerfeed', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='power_feeds', to='tenancy.tenant'), - ), - ] diff --git a/netbox/dcim/migrations/0181_rename_device_role_device_role.py b/netbox/dcim/migrations/0181_rename_device_role_device_role.py deleted file mode 100644 index e32e00221..000000000 --- a/netbox/dcim/migrations/0181_rename_device_role_device_role.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.db import migrations - - -def update_table_configs(apps, schema_editor): - """ - Replace the `device_role` column in DeviceTable configs with `role`. - """ - UserConfig = apps.get_model('users', 'UserConfig') - - for table in ('DeviceTable', 'DeviceBayTable'): - for config in UserConfig.objects.filter(**{f'data__tables__{table}__columns__contains': 'device_role'}): - config.data['tables'][table]['columns'] = [ - 'role' if x == 'device_role' else x - for x in config.data['tables'][table]['columns'] - ] - config.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0180_powerfeed_tenant'), - ] - - operations = [ - migrations.RenameField( - model_name='device', - old_name='device_role', - new_name='role', - ), - migrations.RunPython( - code=update_table_configs, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/migrations/0182_zero_length_cable_fix.py b/netbox/dcim/migrations/0182_zero_length_cable_fix.py deleted file mode 100644 index 080e00717..000000000 --- a/netbox/dcim/migrations/0182_zero_length_cable_fix.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.db import migrations - - -def update_cable_lengths(apps, schema_editor): - Cable = apps.get_model('dcim', 'Cable') - - # Set the absolute length for any zero-length Cables - Cable.objects.filter(length=0).update(_abs_length=0) - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0181_rename_device_role_device_role'), - ] - - operations = [ - migrations.RunPython( - code=update_cable_lengths, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index d1c80d0be..7ae755602 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -317,10 +317,14 @@ class CableTermination(ChangeLoggedModel): super().clean() # Check for existing termination - existing_termination = CableTermination.objects.exclude(cable=self.cable).filter( + qs = CableTermination.objects.filter( termination_type=self.termination_type, termination_id=self.termination_id - ).first() + ) + if self.cable.pk: + qs = qs.exclude(cable=self.cable) + + existing_termination = qs.first() if existing_termination is not None: raise ValidationError( f"Duplicate termination found for {self.termination_type.app_label}.{self.termination_type.model} " diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index ef235078f..88dddb312 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1115,7 +1115,7 @@ class DeviceBay(ComponentModel, TrackingModelMixin): installed_device = models.OneToOneField( to='dcim.Device', on_delete=models.SET_NULL, - related_name=_('parent_bay'), + related_name='parent_bay', blank=True, null=True ) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 4b9689a22..b0edf49fe 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -222,7 +222,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): @property def get_full_name(self): - return f"{ self.manufacturer } { self.model }" + return f"{self.manufacturer} {self.model}" def to_yaml(self): data = { @@ -1098,7 +1098,7 @@ class Device( :param if_master: If True, return VC member interfaces only if this Device is the VC master. """ - filter = Q(device=self) + filter = Q(device=self) if self.pk else Q() if self.virtual_chassis and (self.virtual_chassis.master == self or not if_master): filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False) return Interface.objects.filter(filter) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 1862893ff..835a72bc6 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -21,37 +21,37 @@ WEIGHT = """ """ DEVICE_LINK = """ -{{ value|default:'Unnamed device' }} +{{ value|default:'Unnamed device' }} """ DEVICEBAY_STATUS = """ {% if record.installed_device_id %} - + {{ record.installed_device.get_status_display }} {% else %} - Vacant + Vacant {% endif %} """ INTERFACE_IPADDRESSES = """ -
- {% for ip in value.all %} - {% if ip.status != 'active' %} - {{ ip }} - {% else %} - {{ ip }} - {% endif %} - {% endfor %} -
+ {% if value.count > 3 %} + {{ value.count }} + {% else %} + {% for ip in value.all %} + {% if ip.status != 'active' %} + {{ ip }} + {% else %} + {{ ip }} + {% endif %} + {% endfor %} + {% endif %} """ INTERFACE_FHRPGROUPS = """ - """ INTERFACE_TAGGED_VLANS = """ diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index f36b11033..d012a840f 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -2105,7 +2105,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase): for j in range(0, 13): interfaces.append( # Interface name starts with parent device's position in VC; e.g. 1/1, 1/2, 1/3... - Interface(device=device, name=f'{i%3+1}/{j}', type=InterfaceTypeChoices.TYPE_1GE_FIXED) + Interface(device=device, name=f'{i % 3 + 1}/{j}', type=InterfaceTypeChoices.TYPE_1GE_FIXED) ) Interface.objects.bulk_create(interfaces) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 497935b15..b5a90ced3 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -58,7 +58,11 @@ class DeviceComponentsView(generic.ObjectChildrenView): return self.child_model.objects.restrict(request.user, 'view').filter(device=parent) -class DeviceTypeComponentsView(DeviceComponentsView): +class DeviceTypeComponentsView(generic.ObjectChildrenView): + actions = { + **DEFAULT_ACTION_PERMISSIONS, + 'bulk_rename': {'change'}, + } queryset = DeviceType.objects.all() template_name = 'dcim/devicetype/component_templates.html' viewname = None # Used for return_url resolution @@ -2956,7 +2960,6 @@ class InventoryItemBulkDeleteView(generic.BulkDeleteView): queryset = InventoryItem.objects.all() filterset = filtersets.InventoryItemFilterSet table = tables.InventoryItemTable - template_name = 'dcim/inventoryitem_bulk_delete.html' @register_model_view(InventoryItem, 'children') @@ -3353,6 +3356,7 @@ class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, V formset = VCMemberFormSet(queryset=members_queryset) return render(request, 'dcim/virtualchassis_edit.html', { + 'object': virtual_chassis, 'vc_form': vc_form, 'formset': formset, 'return_url': self.get_return_url(request, virtual_chassis), diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 60a30aed2..714c92548 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -3,6 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers +from rest_framework.fields import ListField from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer from core.api.serializers import JobSerializer @@ -126,11 +127,15 @@ class CustomFieldSerializer(ValidatedModelSerializer): type = ChoiceField(choices=CustomFieldTypeChoices) object_type = ContentTypeField( queryset=ContentType.objects.all(), - required=False + required=False, + allow_null=True ) filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) data_type = serializers.SerializerMethodField() - choice_set = NestedCustomFieldChoiceSetSerializer(required=False) + choice_set = NestedCustomFieldChoiceSetSerializer( + required=False, + allow_null=True + ) ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False) ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False) @@ -171,6 +176,12 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): choices=CustomFieldChoiceSetBaseChoices, required=False ) + extra_choices = serializers.ListField( + child=serializers.ListField( + min_length=2, + max_length=2 + ) + ) class Meta: model = CustomFieldChoiceSet diff --git a/netbox/extras/dashboard/forms.py b/netbox/extras/dashboard/forms.py index ab708228c..fd3492726 100644 --- a/netbox/extras/dashboard/forms.py +++ b/netbox/extras/dashboard/forms.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext as _ from extras.choices import DashboardWidgetColorChoices from netbox.registry import registry -from utilities.forms import BootstrapMixin, add_blank_choice +from utilities.forms import add_blank_choice __all__ = ( 'DashboardWidgetAddForm', @@ -16,7 +16,7 @@ def get_widget_choices(): return registry['widgets'].items() -class DashboardWidgetForm(BootstrapMixin, forms.Form): +class DashboardWidgetForm(forms.Form): title = forms.CharField( required=False ) diff --git a/netbox/extras/dashboard/utils.py b/netbox/extras/dashboard/utils.py index f7ffad2b2..812b8db1b 100644 --- a/netbox/extras/dashboard/utils.py +++ b/netbox/extras/dashboard/utils.py @@ -53,13 +53,13 @@ def get_dashboard(user): return dashboard -def get_default_dashboard(): +def get_default_dashboard(config=None): from extras.models import Dashboard dashboard = Dashboard() - default_config = settings.DEFAULT_DASHBOARD or DEFAULT_DASHBOARD + config = config or settings.DEFAULT_DASHBOARD or DEFAULT_DASHBOARD - for widget in default_config: + for widget in config: id = str(uuid.uuid4()) dashboard.layout.append({ 'id': id, diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 8cfbb4c61..7503b7077 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -15,7 +15,6 @@ from django.utils.translation import gettext as _ from core.models import ContentType from extras.choices import BookmarkOrderingChoices from utilities.choices import ButtonColorChoices -from utilities.forms import BootstrapMixin from utilities.permissions import get_permission_for_model from utilities.templatetags.builtins.filters import render_markdown from utilities.utils import content_type_identifier, content_type_name, dict_to_querydict, get_viewname @@ -58,7 +57,7 @@ def get_models_from_content_types(content_types): return models -class WidgetConfigForm(BootstrapMixin, forms.Form): +class WidgetConfigForm(forms.Form): pass diff --git a/netbox/extras/events.py b/netbox/extras/events.py index 6d0654929..c50f4488d 100644 --- a/netbox/extras/events.py +++ b/netbox/extras/events.py @@ -71,17 +71,17 @@ def enqueue_object(queue, instance, user, request_id, action): }) -def process_event_rules(event_rules, model_name, event, data, username, snapshots=None, request_id=None): - try: +def process_event_rules(event_rules, model_name, event, data, username=None, snapshots=None, request_id=None): + if username: user = get_user_model().objects.get(username=username) - except ObjectDoesNotExist: + else: user = None for event_rule in event_rules: # Evaluate event rule conditions (if any) if not event_rule.eval_conditions(data): - return + continue # Webhooks if event_rule.action_type == EventRuleActionChoices.WEBHOOK: diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 346225c8a..067119aa4 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -13,7 +13,7 @@ from extras.choices import * from extras.models import * from netbox.forms import NetBoxModelForm from tenancy.models import Tenant, TenantGroup -from utilities.forms import BootstrapMixin, add_blank_choice, get_field_value +from utilities.forms import add_blank_choice, get_field_value from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, @@ -38,7 +38,7 @@ __all__ = ( ) -class CustomFieldForm(BootstrapMixin, forms.ModelForm): +class CustomFieldForm(forms.ModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), queryset=ContentType.objects.with_feature('custom_fields') @@ -83,7 +83,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): self.fields['type'].disabled = True -class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm): +class CustomFieldChoiceSetForm(forms.ModelForm): extra_choices = forms.CharField( widget=ChoicesWidget(), required=False, @@ -122,7 +122,7 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm): return data -class CustomLinkForm(BootstrapMixin, forms.ModelForm): +class CustomLinkForm(forms.ModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), queryset=ContentType.objects.with_feature('custom_links') @@ -142,14 +142,16 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm): } help_texts = { 'link_text': _( - "Jinja2 template code for the link text. Reference the object as {{ object }}. Links " + "Jinja2 template code for the link text. Reference the object as {example}. Links " "which render as empty text will not be displayed." - ), - 'link_url': _("Jinja2 template code for the link URL. Reference the object as {{ object }}."), + ).format(example="{{ object }}"), + 'link_url': _( + "Jinja2 template code for the link URL. Reference the object as {example}." + ).format(example="{{ object }}"), } -class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): +class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), queryset=ContentType.objects.with_feature('export_templates') @@ -189,7 +191,7 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): return self.cleaned_data -class SavedFilterForm(BootstrapMixin, forms.ModelForm): +class SavedFilterForm(forms.ModelForm): slug = SlugField() content_types = ContentTypeMultipleChoiceField( label=_('Content types'), @@ -216,7 +218,7 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm): super().__init__(*args, initial=initial, **kwargs) -class BookmarkForm(BootstrapMixin, forms.ModelForm): +class BookmarkForm(forms.ModelForm): object_type = ContentTypeChoiceField( label=_('Object type'), queryset=ContentType.objects.with_feature('bookmarks') @@ -367,7 +369,7 @@ class EventRuleForm(NetBoxModelForm): return super().save(*args, **kwargs) -class TagForm(BootstrapMixin, forms.ModelForm): +class TagForm(forms.ModelForm): slug = SlugField() object_types = ContentTypeMultipleChoiceField( label=_('Object types'), @@ -386,7 +388,7 @@ class TagForm(BootstrapMixin, forms.ModelForm): ] -class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): +class ConfigContextForm(SyncedDataMixin, forms.ModelForm): regions = DynamicModelMultipleChoiceField( label=_('Regions'), queryset=Region.objects.all(), @@ -497,7 +499,7 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): return self.cleaned_data -class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): +class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm): tags = DynamicModelMultipleChoiceField( label=_('Tags'), queryset=Tag.objects.all(), @@ -541,7 +543,7 @@ class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): return self.cleaned_data -class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): +class ImageAttachmentForm(forms.ModelForm): class Meta: model = ImageAttachment diff --git a/netbox/extras/forms/reports.py b/netbox/extras/forms/reports.py index 0ff7d1509..ad37eb744 100644 --- a/netbox/extras/forms/reports.py +++ b/netbox/extras/forms/reports.py @@ -2,7 +2,6 @@ from django import forms from django.utils.translation import gettext_lazy as _ from extras.choices import DurationChoices -from utilities.forms import BootstrapMixin from utilities.forms.widgets import DateTimePicker, NumberWithOptions from utilities.utils import local_now @@ -11,7 +10,7 @@ __all__ = ( ) -class ReportForm(BootstrapMixin, forms.Form): +class ReportForm(forms.Form): schedule_at = forms.DateTimeField( required=False, widget=DateTimePicker(), diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py index c555747ae..f67ad3e75 100644 --- a/netbox/extras/forms/scripts.py +++ b/netbox/extras/forms/scripts.py @@ -2,7 +2,6 @@ from django import forms from django.utils.translation import gettext_lazy as _ from extras.choices import DurationChoices -from utilities.forms import BootstrapMixin from utilities.forms.widgets import DateTimePicker, NumberWithOptions from utilities.utils import local_now @@ -11,7 +10,7 @@ __all__ = ( ) -class ScriptForm(BootstrapMixin, forms.Form): +class ScriptForm(forms.Form): _commit = forms.BooleanField( required=False, initial=True, diff --git a/netbox/extras/migrations/0060_customlink_button_class.py b/netbox/extras/migrations/0060_customlink_button_class.py deleted file mode 100644 index d54df3042..000000000 --- a/netbox/extras/migrations/0060_customlink_button_class.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0059_exporttemplate_as_attachment'), - ] - - operations = [ - migrations.AlterField( - model_name='customlink', - name='button_class', - field=models.CharField(default='outline-dark', max_length=30), - ), - ] diff --git a/netbox/extras/migrations/0060_squashed_0086.py b/netbox/extras/migrations/0060_squashed_0086.py new file mode 100644 index 000000000..0d5d03008 --- /dev/null +++ b/netbox/extras/migrations/0060_squashed_0086.py @@ -0,0 +1,510 @@ +import re +import uuid + +import django.core.validators +import django.db.models.deletion +import taggit.managers +from django.conf import settings +from django.db import migrations, models + +import extras.fields +import utilities.json + + +class Migration(migrations.Migration): + + replaces = [ + ('extras', '0060_customlink_button_class'), + ('extras', '0061_extras_change_logging'), + ('extras', '0062_clear_secrets_changelog'), + ('extras', '0063_webhook_conditions'), + ('extras', '0064_configrevision'), + ('extras', '0065_imageattachment_change_logging'), + ('extras', '0066_customfield_name_validation'), + ('extras', '0067_customfield_min_max_values'), + ('extras', '0068_configcontext_cluster_types'), + ('extras', '0069_custom_object_field'), + ('extras', '0070_customlink_enabled'), + ('extras', '0071_standardize_id_fields'), + ('extras', '0072_created_datetimefield'), + ('extras', '0073_journalentry_tags_custom_fields'), + ('extras', '0074_customfield_extensions'), + ('extras', '0075_configcontext_locations'), + ('extras', '0076_tag_slug_unicode'), + ('extras', '0077_customlink_extend_text_and_url'), + ('extras', '0078_unique_constraints'), + ('extras', '0079_scheduled_jobs'), + ('extras', '0080_customlink_content_types'), + ('extras', '0081_exporttemplate_content_types'), + ('extras', '0082_savedfilter'), + ('extras', '0083_search'), + ('extras', '0084_staging'), + ('extras', '0085_synced_data'), + ('extras', '0086_configtemplate') + ] + + dependencies = [ + ('virtualization', '0001_squashed_0022'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), + ('wireless', '0008_wirelesslan_status'), + ('dcim', '0166_virtualdevicecontext'), + ('tenancy', '0009_standardize_description_comments'), + ('extras', '0059_exporttemplate_as_attachment'), + ('circuits', '0041_standardize_description_comments'), + ] + + operations = [ + migrations.AlterField( + model_name='customlink', + name='button_class', + field=models.CharField(default='outline-dark', max_length=30), + ), + migrations.AddField( + model_name='customfield', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='customfield', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='customlink', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='customlink', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='exporttemplate', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='exporttemplate', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='webhook', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='webhook', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='webhook', + name='conditions', + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name='imageattachment', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AlterField( + model_name='customfield', + name='name', + field=models.CharField(max_length=50, unique=True, validators=[django.core.validators.RegexValidator(flags=re.RegexFlag['IGNORECASE'], message='Only alphanumeric characters and underscores are allowed.', regex='^[a-z0-9_]+$'), django.core.validators.RegexValidator(flags=re.RegexFlag['IGNORECASE'], inverse_match=True, message='Double underscores are not permitted in custom field names.', regex='__')]), + ), + migrations.AlterField( + model_name='customfield', + name='validation_maximum', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='customfield', + name='validation_minimum', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='configcontext', + name='cluster_types', + field=models.ManyToManyField(blank=True, related_name='+', to='virtualization.clustertype'), + ), + migrations.AddField( + model_name='customfield', + name='object_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='customlink', + name='enabled', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='configcontext', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.CreateModel( + name='ConfigRevision', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('comment', models.CharField(blank=True, max_length=200)), + ('data', models.JSONField(blank=True, null=True)), + ], + ), + migrations.AlterField( + model_name='customfield', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='customlink', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='exporttemplate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='imageattachment', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='jobresult', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='journalentry', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='objectchange', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='taggeditem', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='webhook', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='imageattachment', + name='object_id', + field=models.PositiveBigIntegerField(), + ), + migrations.AlterField( + model_name='journalentry', + name='assigned_object_id', + field=models.PositiveBigIntegerField(), + ), + migrations.AlterField( + model_name='objectchange', + name='changed_object_id', + field=models.PositiveBigIntegerField(), + ), + migrations.AlterField( + model_name='objectchange', + name='related_object_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='configcontext', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='customfield', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='customlink', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='exporttemplate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='imageattachment', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='journalentry', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='tag', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='webhook', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='journalentry', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), + ), + migrations.AddField( + model_name='journalentry', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterModelOptions( + name='customfield', + options={'ordering': ['group_name', 'weight', 'name']}, + ), + migrations.AddField( + model_name='customfield', + name='group_name', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='customfield', + name='ui_visibility', + field=models.CharField(default='read-write', max_length=50), + ), + migrations.AddField( + model_name='configcontext', + name='locations', + field=models.ManyToManyField(blank=True, related_name='+', to='dcim.location'), + ), + migrations.AlterField( + model_name='tag', + name='slug', + field=models.SlugField(allow_unicode=True, max_length=100, unique=True), + ), + migrations.AlterField( + model_name='customlink', + name='link_text', + field=models.TextField(), + ), + migrations.AlterField( + model_name='customlink', + name='link_url', + field=models.TextField(), + ), + migrations.AlterUniqueTogether( + name='exporttemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='webhook', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='exporttemplate', + constraint=models.UniqueConstraint(fields=('content_type', 'name'), name='extras_exporttemplate_unique_content_type_name'), + ), + migrations.AddConstraint( + model_name='webhook', + constraint=models.UniqueConstraint(fields=('payload_url', 'type_create', 'type_update', 'type_delete'), name='extras_webhook_unique_payload_url_types'), + ), + migrations.AddField( + model_name='jobresult', + name='scheduled', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='jobresult', + name='interval', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='jobresult', + name='started', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterModelOptions( + name='jobresult', + options={'ordering': ['-created']}, + ), + migrations.AddField( + model_name='customlink', + name='content_types', + field=models.ManyToManyField(related_name='custom_links', to='contenttypes.contenttype'), + ), + migrations.RemoveField( + model_name='customlink', + name='content_type', + ), + migrations.AddField( + model_name='exporttemplate', + name='content_types', + field=models.ManyToManyField(related_name='export_templates', to='contenttypes.contenttype'), + ), + migrations.RemoveConstraint( + model_name='exporttemplate', + name='extras_exporttemplate_unique_content_type_name', + ), + migrations.RemoveField( + model_name='exporttemplate', + name='content_type', + ), + migrations.AlterModelOptions( + name='exporttemplate', + options={'ordering': ('name',)}, + ), + migrations.CreateModel( + name='SavedFilter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(max_length=100, unique=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('weight', models.PositiveSmallIntegerField(default=100)), + ('enabled', models.BooleanField(default=True)), + ('shared', models.BooleanField(default=True)), + ('parameters', models.JSONField()), + ('content_types', models.ManyToManyField(related_name='saved_filters', to='contenttypes.contenttype')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('weight', 'name'), + }, + ), + migrations.AddField( + model_name='customfield', + name='search_weight', + field=models.PositiveSmallIntegerField(default=1000), + ), + migrations.CreateModel( + name='CachedValue', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('object_id', models.PositiveBigIntegerField()), + ('field', models.CharField(max_length=200)), + ('type', models.CharField(max_length=30)), + ('value', extras.fields.CachedValueField()), + ('weight', models.PositiveSmallIntegerField(default=1000)), + ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), + ], + options={ + 'ordering': ('weight', 'object_type', 'object_id'), + }, + ), + migrations.CreateModel( + name='Branch', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=100, unique=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='StagedChange', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('action', models.CharField(max_length=20)), + ('object_id', models.PositiveBigIntegerField(blank=True, null=True)), + ('data', models.JSONField(blank=True, null=True)), + ('branch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staged_changes', to='extras.branch')), + ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), + ], + options={ + 'ordering': ('pk',), + }, + ), + migrations.AddField( + model_name='configcontext', + name='data_file', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile'), + ), + migrations.AddField( + model_name='configcontext', + name='data_path', + field=models.CharField(blank=True, editable=False, max_length=1000), + ), + migrations.AddField( + model_name='configcontext', + name='data_source', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'), + ), + migrations.AddField( + model_name='configcontext', + name='auto_sync_enabled', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='configcontext', + name='data_synced', + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_file', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile'), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_path', + field=models.CharField(blank=True, editable=False, max_length=1000), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_source', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'), + ), + migrations.AddField( + model_name='exporttemplate', + name='auto_sync_enabled', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_synced', + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.CreateModel( + name='ConfigTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('data_path', models.CharField(blank=True, editable=False, max_length=1000)), + ('data_synced', models.DateTimeField(blank=True, editable=False, null=True)), + ('name', models.CharField(max_length=100)), + ('description', models.CharField(blank=True, max_length=200)), + ('template_code', models.TextField()), + ('environment_params', models.JSONField(blank=True, default=dict, null=True)), + ('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')), + ('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')), + ('auto_sync_enabled', models.BooleanField(default=False)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ('name',), + }, + ), + ] diff --git a/netbox/extras/migrations/0061_extras_change_logging.py b/netbox/extras/migrations/0061_extras_change_logging.py deleted file mode 100644 index 4ee532fd5..000000000 --- a/netbox/extras/migrations/0061_extras_change_logging.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0060_customlink_button_class'), - ] - - operations = [ - migrations.AddField( - model_name='customfield', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='customfield', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='customlink', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='customlink', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='exporttemplate', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='exporttemplate', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - migrations.AddField( - model_name='webhook', - name='created', - field=models.DateField(auto_now_add=True, null=True), - ), - migrations.AddField( - model_name='webhook', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - ] diff --git a/netbox/extras/migrations/0062_clear_secrets_changelog.py b/netbox/extras/migrations/0062_clear_secrets_changelog.py deleted file mode 100644 index e76fc8d34..000000000 --- a/netbox/extras/migrations/0062_clear_secrets_changelog.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.db import migrations - - -def clear_secrets_changelog(apps, schema_editor): - """ - Delete all ObjectChange records referencing a model within the old secrets app (pre-v3.0). - """ - ContentType = apps.get_model('contenttypes', 'ContentType') - ObjectChange = apps.get_model('extras', 'ObjectChange') - - content_type_ids = ContentType.objects.filter(app_label='secrets').values_list('id', flat=True) - ObjectChange.objects.filter(changed_object_type__in=content_type_ids).delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0061_extras_change_logging'), - ] - - operations = [ - migrations.RunPython( - code=clear_secrets_changelog, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/extras/migrations/0063_webhook_conditions.py b/netbox/extras/migrations/0063_webhook_conditions.py deleted file mode 100644 index 8cc5b1bd3..000000000 --- a/netbox/extras/migrations/0063_webhook_conditions.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-22 20:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0062_clear_secrets_changelog'), - ] - - operations = [ - migrations.AddField( - model_name='webhook', - name='conditions', - field=models.JSONField(blank=True, null=True), - ), - ] diff --git a/netbox/extras/migrations/0064_configrevision.py b/netbox/extras/migrations/0064_configrevision.py deleted file mode 100644 index c3fce8abe..000000000 --- a/netbox/extras/migrations/0064_configrevision.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0063_webhook_conditions'), - ] - - operations = [ - migrations.CreateModel( - name='ConfigRevision', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True)), - ('comment', models.CharField(blank=True, max_length=200)), - ('data', models.JSONField(blank=True, null=True)), - ], - ), - ] diff --git a/netbox/extras/migrations/0065_imageattachment_change_logging.py b/netbox/extras/migrations/0065_imageattachment_change_logging.py deleted file mode 100644 index dc623e46c..000000000 --- a/netbox/extras/migrations/0065_imageattachment_change_logging.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0064_configrevision'), - ] - - operations = [ - migrations.AddField( - model_name='imageattachment', - name='last_updated', - field=models.DateTimeField(auto_now=True, null=True), - ), - ] diff --git a/netbox/extras/migrations/0066_customfield_name_validation.py b/netbox/extras/migrations/0066_customfield_name_validation.py deleted file mode 100644 index 3d2c51399..000000000 --- a/netbox/extras/migrations/0066_customfield_name_validation.py +++ /dev/null @@ -1,34 +0,0 @@ -import django.core.validators -from django.db import migrations, models -import re - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0065_imageattachment_change_logging'), - ] - - operations = [ - migrations.AlterField( - model_name='customfield', - name='name', - field=models.CharField( - max_length=50, - unique=True, - validators=[ - django.core.validators.RegexValidator( - flags=re.RegexFlag['IGNORECASE'], - message='Only alphanumeric characters and underscores are allowed.', - regex='^[a-z0-9_]+$', - ), - django.core.validators.RegexValidator( - flags=re.RegexFlag['IGNORECASE'], - inverse_match=True, - message='Double underscores are not permitted in custom field names.', - regex=r'__', - ), - ], - ), - ), - ] diff --git a/netbox/extras/migrations/0067_customfield_min_max_values.py b/netbox/extras/migrations/0067_customfield_min_max_values.py deleted file mode 100644 index cec4f6ae0..000000000 --- a/netbox/extras/migrations/0067_customfield_min_max_values.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0066_customfield_name_validation'), - ] - - operations = [ - migrations.AlterField( - model_name='customfield', - name='validation_maximum', - field=models.IntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='customfield', - name='validation_minimum', - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/extras/migrations/0068_configcontext_cluster_types.py b/netbox/extras/migrations/0068_configcontext_cluster_types.py deleted file mode 100644 index 3d314991d..000000000 --- a/netbox/extras/migrations/0068_configcontext_cluster_types.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0145_site_remove_deprecated_fields'), - ('virtualization', '0026_vminterface_bridge'), - ('extras', '0067_customfield_min_max_values'), - ] - - operations = [ - migrations.AddField( - model_name='configcontext', - name='cluster_types', - field=models.ManyToManyField(blank=True, related_name='+', to='virtualization.ClusterType'), - ), - ] diff --git a/netbox/extras/migrations/0069_custom_object_field.py b/netbox/extras/migrations/0069_custom_object_field.py deleted file mode 100644 index 720e21edc..000000000 --- a/netbox/extras/migrations/0069_custom_object_field.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('extras', '0068_configcontext_cluster_types'), - ] - - operations = [ - migrations.AddField( - model_name='customfield', - name='object_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype'), - ), - ] diff --git a/netbox/extras/migrations/0070_customlink_enabled.py b/netbox/extras/migrations/0070_customlink_enabled.py deleted file mode 100644 index 839a4dba5..000000000 --- a/netbox/extras/migrations/0070_customlink_enabled.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.11 on 2022-01-10 16:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0069_custom_object_field'), - ] - - operations = [ - migrations.AddField( - model_name='customlink', - name='enabled', - field=models.BooleanField(default=True), - ), - ] diff --git a/netbox/extras/migrations/0071_standardize_id_fields.py b/netbox/extras/migrations/0071_standardize_id_fields.py deleted file mode 100644 index 63e3051d8..000000000 --- a/netbox/extras/migrations/0071_standardize_id_fields.py +++ /dev/null @@ -1,89 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0070_customlink_enabled'), - ] - - operations = [ - # Model IDs - migrations.AlterField( - model_name='configcontext', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='configrevision', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='customfield', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='customlink', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='exporttemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='imageattachment', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='jobresult', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='journalentry', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='objectchange', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='taggeditem', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='webhook', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - - # GFK IDs - migrations.AlterField( - model_name='imageattachment', - name='object_id', - field=models.PositiveBigIntegerField(), - ), - migrations.AlterField( - model_name='journalentry', - name='assigned_object_id', - field=models.PositiveBigIntegerField(), - ), - migrations.AlterField( - model_name='objectchange', - name='changed_object_id', - field=models.PositiveBigIntegerField(), - ), - migrations.AlterField( - model_name='objectchange', - name='related_object_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/extras/migrations/0072_created_datetimefield.py b/netbox/extras/migrations/0072_created_datetimefield.py deleted file mode 100644 index 827e99e54..000000000 --- a/netbox/extras/migrations/0072_created_datetimefield.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 4.0.2 on 2022-02-08 18:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0071_standardize_id_fields'), - ] - - operations = [ - migrations.AlterField( - model_name='configcontext', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='customfield', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='customlink', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='exporttemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='imageattachment', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='journalentry', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='tag', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='webhook', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - ] diff --git a/netbox/extras/migrations/0073_journalentry_tags_custom_fields.py b/netbox/extras/migrations/0073_journalentry_tags_custom_fields.py deleted file mode 100644 index 5f2d7f7f3..000000000 --- a/netbox/extras/migrations/0073_journalentry_tags_custom_fields.py +++ /dev/null @@ -1,23 +0,0 @@ -from utilities.json import CustomFieldJSONEncoder -from django.db import migrations, models -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0072_created_datetimefield'), - ] - - operations = [ - migrations.AddField( - model_name='journalentry', - name='custom_field_data', - field=models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder), - ), - migrations.AddField( - model_name='journalentry', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - ] diff --git a/netbox/extras/migrations/0074_customfield_extensions.py b/netbox/extras/migrations/0074_customfield_extensions.py deleted file mode 100644 index 6ca8b958f..000000000 --- a/netbox/extras/migrations/0074_customfield_extensions.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.0.4 on 2022-04-15 17:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0073_journalentry_tags_custom_fields'), - ] - - operations = [ - migrations.AlterModelOptions( - name='customfield', - options={'ordering': ['group_name', 'weight', 'name']}, - ), - migrations.AddField( - model_name='customfield', - name='group_name', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='customfield', - name='ui_visibility', - field=models.CharField(default='read-write', max_length=50), - ), - ] diff --git a/netbox/extras/migrations/0075_configcontext_locations.py b/netbox/extras/migrations/0075_configcontext_locations.py deleted file mode 100644 index 853aec4f7..000000000 --- a/netbox/extras/migrations/0075_configcontext_locations.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.0.5 on 2022-06-22 19:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0156_location_status'), - ('extras', '0074_customfield_extensions'), - ] - - operations = [ - migrations.AddField( - model_name='configcontext', - name='locations', - field=models.ManyToManyField(blank=True, related_name='+', to='dcim.location'), - ), - ] diff --git a/netbox/extras/migrations/0076_tag_slug_unicode.py b/netbox/extras/migrations/0076_tag_slug_unicode.py deleted file mode 100644 index 3f4922963..000000000 --- a/netbox/extras/migrations/0076_tag_slug_unicode.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.6 on 2022-07-14 15:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0075_configcontext_locations'), - ] - - operations = [ - migrations.AlterField( - model_name='tag', - name='slug', - field=models.SlugField(allow_unicode=True, max_length=100, unique=True), - ), - ] diff --git a/netbox/extras/migrations/0077_customlink_extend_text_and_url.py b/netbox/extras/migrations/0077_customlink_extend_text_and_url.py deleted file mode 100644 index c08948aa6..000000000 --- a/netbox/extras/migrations/0077_customlink_extend_text_and_url.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0076_tag_slug_unicode'), - ] - - operations = [ - migrations.AlterField( - model_name='customlink', - name='link_text', - field=models.TextField(), - ), - migrations.AlterField( - model_name='customlink', - name='link_url', - field=models.TextField(), - ), - ] diff --git a/netbox/extras/migrations/0078_unique_constraints.py b/netbox/extras/migrations/0078_unique_constraints.py deleted file mode 100644 index 4a56831a7..000000000 --- a/netbox/extras/migrations/0078_unique_constraints.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0077_customlink_extend_text_and_url'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='exporttemplate', - unique_together=set(), - ), - migrations.AlterUniqueTogether( - name='webhook', - unique_together=set(), - ), - migrations.AddConstraint( - model_name='exporttemplate', - constraint=models.UniqueConstraint(fields=('content_type', 'name'), name='extras_exporttemplate_unique_content_type_name'), - ), - migrations.AddConstraint( - model_name='webhook', - constraint=models.UniqueConstraint(fields=('payload_url', 'type_create', 'type_update', 'type_delete'), name='extras_webhook_unique_payload_url_types'), - ), - ] diff --git a/netbox/extras/migrations/0079_scheduled_jobs.py b/netbox/extras/migrations/0079_scheduled_jobs.py deleted file mode 100644 index f9f8c6357..000000000 --- a/netbox/extras/migrations/0079_scheduled_jobs.py +++ /dev/null @@ -1,31 +0,0 @@ -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0078_unique_constraints'), - ] - - operations = [ - migrations.AddField( - model_name='jobresult', - name='scheduled', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='jobresult', - name='interval', - field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), - ), - migrations.AddField( - model_name='jobresult', - name='started', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterModelOptions( - name='jobresult', - options={'ordering': ['-created']}, - ), - ] diff --git a/netbox/extras/migrations/0080_customlink_content_types.py b/netbox/extras/migrations/0080_customlink_content_types.py deleted file mode 100644 index 7f8456c67..000000000 --- a/netbox/extras/migrations/0080_customlink_content_types.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.db import migrations, models - - -def copy_content_types(apps, schema_editor): - CustomLink = apps.get_model('extras', 'CustomLink') - - for customlink in CustomLink.objects.all(): - customlink.content_types.set([customlink.content_type]) - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('extras', '0079_scheduled_jobs'), - ] - - operations = [ - migrations.AddField( - model_name='customlink', - name='content_types', - field=models.ManyToManyField(related_name='custom_links', to='contenttypes.contenttype'), - ), - migrations.RunPython( - code=copy_content_types, - reverse_code=migrations.RunPython.noop - ), - migrations.RemoveField( - model_name='customlink', - name='content_type', - ), - ] diff --git a/netbox/extras/migrations/0081_exporttemplate_content_types.py b/netbox/extras/migrations/0081_exporttemplate_content_types.py deleted file mode 100644 index afa21c5b8..000000000 --- a/netbox/extras/migrations/0081_exporttemplate_content_types.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.db import migrations, models - - -def copy_content_types(apps, schema_editor): - ExportTemplate = apps.get_model('extras', 'ExportTemplate') - - for et in ExportTemplate.objects.all(): - et.content_types.set([et.content_type]) - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('extras', '0080_customlink_content_types'), - ] - - operations = [ - migrations.AddField( - model_name='exporttemplate', - name='content_types', - field=models.ManyToManyField(related_name='export_templates', to='contenttypes.contenttype'), - ), - migrations.RunPython( - code=copy_content_types, - reverse_code=migrations.RunPython.noop - ), - migrations.RemoveConstraint( - model_name='exporttemplate', - name='extras_exporttemplate_unique_content_type_name', - ), - migrations.RemoveField( - model_name='exporttemplate', - name='content_type', - ), - migrations.AlterModelOptions( - name='exporttemplate', - options={'ordering': ('name',)}, - ), - ] diff --git a/netbox/extras/migrations/0082_savedfilter.py b/netbox/extras/migrations/0082_savedfilter.py deleted file mode 100644 index e2626ec6a..000000000 --- a/netbox/extras/migrations/0082_savedfilter.py +++ /dev/null @@ -1,35 +0,0 @@ -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contenttypes', '0002_remove_content_type_name'), - ('extras', '0081_exporttemplate_content_types'), - ] - - operations = [ - migrations.CreateModel( - name='SavedFilter', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('name', models.CharField(max_length=100, unique=True)), - ('slug', models.SlugField(max_length=100, unique=True)), - ('description', models.CharField(blank=True, max_length=200)), - ('weight', models.PositiveSmallIntegerField(default=100)), - ('enabled', models.BooleanField(default=True)), - ('shared', models.BooleanField(default=True)), - ('parameters', models.JSONField()), - ('content_types', models.ManyToManyField(related_name='saved_filters', to='contenttypes.contenttype')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ('weight', 'name'), - }, - ), - ] diff --git a/netbox/extras/migrations/0083_search.py b/netbox/extras/migrations/0083_search.py deleted file mode 100644 index 4c7ae1084..000000000 --- a/netbox/extras/migrations/0083_search.py +++ /dev/null @@ -1,44 +0,0 @@ -import uuid - -import django.db.models.deletion -import django.db.models.lookups -from django.db import migrations, models -import extras.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0041_standardize_description_comments'), - ('contenttypes', '0002_remove_content_type_name'), - ('dcim', '0166_virtualdevicecontext'), - ('extras', '0082_savedfilter'), - ('ipam', '0063_standardize_description_comments'), - ('tenancy', '0009_standardize_description_comments'), - ('virtualization', '0034_standardize_description_comments'), - ('wireless', '0008_wirelesslan_status'), - ] - - operations = [ - migrations.AddField( - model_name='customfield', - name='search_weight', - field=models.PositiveSmallIntegerField(default=1000), - ), - migrations.CreateModel( - name='CachedValue', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('object_id', models.PositiveBigIntegerField()), - ('field', models.CharField(max_length=200)), - ('type', models.CharField(max_length=30)), - ('value', extras.fields.CachedValueField()), - ('weight', models.PositiveSmallIntegerField(default=1000)), - ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), - ], - options={ - 'ordering': ('weight', 'object_type', 'object_id'), - }, - ), - ] diff --git a/netbox/extras/migrations/0084_staging.py b/netbox/extras/migrations/0084_staging.py deleted file mode 100644 index 3129d7f5b..000000000 --- a/netbox/extras/migrations/0084_staging.py +++ /dev/null @@ -1,45 +0,0 @@ -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('extras', '0083_search'), - ] - - operations = [ - migrations.CreateModel( - name='Branch', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('name', models.CharField(max_length=100, unique=True)), - ('description', models.CharField(blank=True, max_length=200)), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ('name',), - }, - ), - migrations.CreateModel( - name='StagedChange', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('action', models.CharField(max_length=20)), - ('object_id', models.PositiveBigIntegerField(blank=True, null=True)), - ('data', models.JSONField(blank=True, null=True)), - ('branch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staged_changes', to='extras.branch')), - ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), - ], - options={ - 'ordering': ('pk',), - }, - ), - ] diff --git a/netbox/extras/migrations/0085_synced_data.py b/netbox/extras/migrations/0085_synced_data.py deleted file mode 100644 index 372845490..000000000 --- a/netbox/extras/migrations/0085_synced_data.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ('extras', '0084_staging'), - ] - - operations = [ - # ConfigContexts - migrations.AddField( - model_name='configcontext', - name='data_file', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile'), - ), - migrations.AddField( - model_name='configcontext', - name='data_path', - field=models.CharField(blank=True, editable=False, max_length=1000), - ), - migrations.AddField( - model_name='configcontext', - name='data_source', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'), - ), - migrations.AddField( - model_name='configcontext', - name='auto_sync_enabled', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='configcontext', - name='data_synced', - field=models.DateTimeField(blank=True, editable=False, null=True), - ), - # ExportTemplates - migrations.AddField( - model_name='exporttemplate', - name='data_file', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile'), - ), - migrations.AddField( - model_name='exporttemplate', - name='data_path', - field=models.CharField(blank=True, editable=False, max_length=1000), - ), - migrations.AddField( - model_name='exporttemplate', - name='data_source', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'), - ), - migrations.AddField( - model_name='exporttemplate', - name='auto_sync_enabled', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='exporttemplate', - name='data_synced', - field=models.DateTimeField(blank=True, editable=False, null=True), - ), - ] diff --git a/netbox/extras/migrations/0086_configtemplate.py b/netbox/extras/migrations/0086_configtemplate.py deleted file mode 100644 index 32d4e9858..000000000 --- a/netbox/extras/migrations/0086_configtemplate.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ('extras', '0085_synced_data'), - ] - - operations = [ - migrations.CreateModel( - name='ConfigTemplate', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('data_path', models.CharField(blank=True, editable=False, max_length=1000)), - ('data_synced', models.DateTimeField(blank=True, editable=False, null=True)), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=200)), - ('template_code', models.TextField()), - ('environment_params', models.JSONField(blank=True, default=dict, null=True)), - ('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')), - ('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')), - ('auto_sync_enabled', models.BooleanField(default=False)), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ], - options={ - 'ordering': ('name',), - }, - ), - ] diff --git a/netbox/extras/migrations/0087_dashboard.py b/netbox/extras/migrations/0087_dashboard.py deleted file mode 100644 index 0c048c5ef..000000000 --- a/netbox/extras/migrations/0087_dashboard.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.1.7 on 2023-02-24 00:56 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('extras', '0086_configtemplate'), - ] - - operations = [ - migrations.CreateModel( - name='Dashboard', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('layout', models.JSONField(default=list)), - ('config', models.JSONField(default=dict)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dashboard', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/netbox/extras/migrations/0087_squashed_0098.py b/netbox/extras/migrations/0087_squashed_0098.py new file mode 100644 index 000000000..57ad3af00 --- /dev/null +++ b/netbox/extras/migrations/0087_squashed_0098.py @@ -0,0 +1,156 @@ +import django.contrib.postgres.fields +import django.db.models.deletion +import taggit.managers +from django.conf import settings +from django.db import migrations, models + +import extras.models.mixins +import utilities.json + + +class Migration(migrations.Migration): + + replaces = [ + ('extras', '0087_dashboard'), + ('extras', '0088_jobresult_webhooks'), + ('extras', '0089_customfield_is_cloneable'), + ('extras', '0090_objectchange_index_request_id'), + ('extras', '0091_create_managedfiles'), + ('extras', '0092_delete_jobresult'), + ('extras', '0093_configrevision_ordering'), + ('extras', '0094_tag_object_types'), + ('extras', '0095_bookmarks'), + ('extras', '0096_customfieldchoiceset'), + ('extras', '0097_customfield_remove_choices'), + ('extras', '0098_webhook_custom_field_data_webhook_tags') + ] + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('extras', '0086_configtemplate'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0002_managedfile'), + ] + + operations = [ + migrations.CreateModel( + name='Dashboard', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('layout', models.JSONField(default=list)), + ('config', models.JSONField(default=dict)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dashboard', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='webhook', + name='type_job_end', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='webhook', + name='type_job_start', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='customfield', + name='is_cloneable', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='objectchange', + name='request_id', + field=models.UUIDField(db_index=True, editable=False), + ), + migrations.CreateModel( + name='ReportModule', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=(extras.models.mixins.PythonModuleMixin, 'core.managedfile', models.Model), + ), + migrations.CreateModel( + name='ScriptModule', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=(extras.models.mixins.PythonModuleMixin, 'core.managedfile', models.Model), + ), + migrations.DeleteModel( + name='JobResult', + ), + migrations.AlterModelOptions( + name='configrevision', + options={'ordering': ['-created']}, + ), + migrations.AddField( + model_name='tag', + name='object_types', + field=models.ManyToManyField(blank=True, related_name='+', to='contenttypes.contenttype'), + ), + migrations.RenameIndex( + model_name='taggeditem', + new_name='extras_tagg_content_717743_idx', + old_fields=('content_type', 'object_id'), + ), + migrations.CreateModel( + name='Bookmark', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('object_id', models.PositiveBigIntegerField()), + ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('created', 'pk'), + }, + ), + migrations.AddConstraint( + model_name='bookmark', + constraint=models.UniqueConstraint(fields=('object_type', 'object_id', 'user'), name='extras_bookmark_unique_per_object_and_user'), + ), + migrations.CreateModel( + name='CustomFieldChoiceSet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=100, unique=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('base_choices', models.CharField(blank=True, max_length=50)), + ('extra_choices', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), size=2), blank=True, null=True, size=None)), + ('order_alphabetically', models.BooleanField(default=False)), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.AddField( + model_name='customfield', + name='choice_set', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='choices_for', to='extras.customfieldchoiceset'), + ), + migrations.RemoveField( + model_name='customfield', + name='choices', + ), + migrations.AddField( + model_name='webhook', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), + ), + migrations.AddField( + model_name='webhook', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/extras/migrations/0088_jobresult_webhooks.py b/netbox/extras/migrations/0088_jobresult_webhooks.py deleted file mode 100644 index 112bcca8c..000000000 --- a/netbox/extras/migrations/0088_jobresult_webhooks.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.1.7 on 2023-02-28 19:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0087_dashboard'), - ] - - operations = [ - migrations.AddField( - model_name='webhook', - name='type_job_end', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='webhook', - name='type_job_start', - field=models.BooleanField(default=False), - ), - ] diff --git a/netbox/extras/migrations/0089_customfield_is_cloneable.py b/netbox/extras/migrations/0089_customfield_is_cloneable.py deleted file mode 100644 index 7f577b45a..000000000 --- a/netbox/extras/migrations/0089_customfield_is_cloneable.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.2 on 2022-11-17 18:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0088_jobresult_webhooks'), - ] - - operations = [ - migrations.AddField( - model_name='customfield', - name='is_cloneable', - field=models.BooleanField(default=False), - ), - ] diff --git a/netbox/extras/migrations/0090_objectchange_index_request_id.py b/netbox/extras/migrations/0090_objectchange_index_request_id.py deleted file mode 100644 index 00e8fde42..000000000 --- a/netbox/extras/migrations/0090_objectchange_index_request_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-16 20:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0089_customfield_is_cloneable'), - ] - - operations = [ - migrations.AlterField( - model_name='objectchange', - name='request_id', - field=models.UUIDField(db_index=True, editable=False), - ), - ] diff --git a/netbox/extras/migrations/0091_create_managedfiles.py b/netbox/extras/migrations/0091_create_managedfiles.py deleted file mode 100644 index 79a80821f..000000000 --- a/netbox/extras/migrations/0091_create_managedfiles.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -import pkgutil - -from django.conf import settings -from django.db import migrations, models -import extras.models.mixins - - -def create_files(cls, root_name, root_path): - - modules = list(pkgutil.iter_modules([root_path])) - filenames = [] - for importer, module_name, ispkg in modules: - try: - module = importer.find_module(module_name).load_module(module_name) - rel_path = os.path.relpath(module.__file__, root_path) - filenames.append(rel_path) - except ImportError: - pass - - managed_files = [ - cls(file_root=root_name, file_path=filename) - for filename in filenames - ] - cls.objects.bulk_create(managed_files) - - -def replicate_scripts(apps, schema_editor): - ScriptModule = apps.get_model('extras', 'ScriptModule') - create_files(ScriptModule, 'scripts', settings.SCRIPTS_ROOT) - - -def replicate_reports(apps, schema_editor): - ReportModule = apps.get_model('extras', 'ReportModule') - create_files(ReportModule, 'reports', settings.REPORTS_ROOT) - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0002_managedfile'), - ('extras', '0090_objectchange_index_request_id'), - ] - - operations = [ - # Create proxy models - migrations.CreateModel( - name='ReportModule', - fields=[ - ], - options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], - }, - bases=(extras.models.mixins.PythonModuleMixin, 'core.managedfile', models.Model), - ), - migrations.CreateModel( - name='ScriptModule', - fields=[ - ], - options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], - }, - bases=(extras.models.mixins.PythonModuleMixin, 'core.managedfile', models.Model), - ), - - # Instantiate ManagedFiles to represent scripts & reports - migrations.RunPython( - code=replicate_scripts, - reverse_code=migrations.RunPython.noop - ), - migrations.RunPython( - code=replicate_reports, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/extras/migrations/0092_delete_jobresult.py b/netbox/extras/migrations/0092_delete_jobresult.py deleted file mode 100644 index c1b121c69..000000000 --- a/netbox/extras/migrations/0092_delete_jobresult.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-27 17:31 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0091_create_managedfiles'), - ] - - operations = [ - migrations.DeleteModel( - name='JobResult', - ), - ] diff --git a/netbox/extras/migrations/0093_configrevision_ordering.py b/netbox/extras/migrations/0093_configrevision_ordering.py deleted file mode 100644 index a4e875e6d..000000000 --- a/netbox/extras/migrations/0093_configrevision_ordering.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.1.9 on 2023-06-22 14:14 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0092_delete_jobresult'), - ] - - operations = [ - migrations.AlterModelOptions( - name='configrevision', - options={'ordering': ['-created']}, - ), - ] diff --git a/netbox/extras/migrations/0094_tag_object_types.py b/netbox/extras/migrations/0094_tag_object_types.py deleted file mode 100644 index 8bb760980..000000000 --- a/netbox/extras/migrations/0094_tag_object_types.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('extras', '0093_configrevision_ordering'), - ] - - operations = [ - migrations.AddField( - model_name='tag', - name='object_types', - field=models.ManyToManyField(blank=True, related_name='+', to='contenttypes.contenttype'), - ), - migrations.RenameIndex( - model_name='taggeditem', - new_name='extras_tagg_content_717743_idx', - old_fields=('content_type', 'object_id'), - ), - ] diff --git a/netbox/extras/migrations/0095_bookmarks.py b/netbox/extras/migrations/0095_bookmarks.py deleted file mode 100644 index 54c14c496..000000000 --- a/netbox/extras/migrations/0095_bookmarks.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 4.1.9 on 2023-06-29 14:07 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contenttypes', '0002_remove_content_type_name'), - ('extras', '0094_tag_object_types'), - ] - - operations = [ - migrations.CreateModel( - name='Bookmark', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True)), - ('object_id', models.PositiveBigIntegerField()), - ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ('created', 'pk'), - }, - ), - migrations.AddConstraint( - model_name='bookmark', - constraint=models.UniqueConstraint(fields=('object_type', 'object_id', 'user'), name='extras_bookmark_unique_per_object_and_user'), - ), - ] diff --git a/netbox/extras/migrations/0096_customfieldchoiceset.py b/netbox/extras/migrations/0096_customfieldchoiceset.py deleted file mode 100644 index 1984e17f8..000000000 --- a/netbox/extras/migrations/0096_customfieldchoiceset.py +++ /dev/null @@ -1,62 +0,0 @@ -import django.contrib.postgres.fields -from django.db import migrations, models - -from extras.choices import CustomFieldTypeChoices - - -def create_choice_sets(apps, schema_editor): - """ - Create a CustomFieldChoiceSet for each CustomField with choices defined. - """ - CustomField = apps.get_model('extras', 'CustomField') - CustomFieldChoiceSet = apps.get_model('extras', 'CustomFieldChoiceSet') - - # Create custom field choice sets - choice_fields = CustomField.objects.filter( - type__in=(CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT), - choices__len__gt=0 - ) - for cf in choice_fields: - choiceset = CustomFieldChoiceSet.objects.create( - name=f'{cf.name} Choices', - extra_choices=tuple(zip(cf.choices, cf.choices)) # Convert list to tuple of two-tuples - ) - cf.choice_set = choiceset - - # Update custom fields to point to new choice sets - CustomField.objects.bulk_update(choice_fields, ['choice_set']) - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0095_bookmarks'), - ] - - operations = [ - migrations.CreateModel( - name='CustomFieldChoiceSet', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('name', models.CharField(max_length=100, unique=True)), - ('description', models.CharField(blank=True, max_length=200)), - ('base_choices', models.CharField(blank=True, max_length=50)), - ('extra_choices', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), size=2), blank=True, null=True, size=None)), - ('order_alphabetically', models.BooleanField(default=False)), - ], - options={ - 'ordering': ('name',), - }, - ), - migrations.AddField( - model_name='customfield', - name='choice_set', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='choices_for', to='extras.customfieldchoiceset'), - ), - migrations.RunPython( - code=create_choice_sets, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/extras/migrations/0097_customfield_remove_choices.py b/netbox/extras/migrations/0097_customfield_remove_choices.py deleted file mode 100644 index f3e8c547e..000000000 --- a/netbox/extras/migrations/0097_customfield_remove_choices.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.1.10 on 2023-07-17 15:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0096_customfieldchoiceset'), - ] - - operations = [ - migrations.RemoveField( - model_name='customfield', - name='choices', - ), - ] diff --git a/netbox/extras/migrations/0098_webhook_custom_field_data_webhook_tags.py b/netbox/extras/migrations/0098_webhook_custom_field_data_webhook_tags.py deleted file mode 100644 index 3fd294388..000000000 --- a/netbox/extras/migrations/0098_webhook_custom_field_data_webhook_tags.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.1.10 on 2023-08-01 16:32 - -from django.db import migrations, models -import taggit.managers -import utilities.json - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0097_customfield_remove_choices'), - ] - - operations = [ - migrations.AddField( - model_name='webhook', - name='custom_field_data', - field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), - ), - migrations.AddField( - model_name='webhook', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - ] diff --git a/netbox/extras/migrations/0106_bookmark_user_cascade_deletion.py b/netbox/extras/migrations/0106_bookmark_user_cascade_deletion.py new file mode 100644 index 000000000..d7bef2f0b --- /dev/null +++ b/netbox/extras/migrations/0106_bookmark_user_cascade_deletion.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.9 on 2024-01-19 19:46 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('extras', '0105_customfield_min_max_values'), + ] + + operations = [ + migrations.AlterField( + model_name='bookmark', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index 425c1386a..6ea2167c2 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -182,7 +182,7 @@ class ConfigContextModel(models.Model): if not hasattr(self, 'config_context_data'): # The annotation is not available, so we fall back to manually querying for the config context objects - config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) + config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) or [] else: # The attribute may exist, but the annotated value could be None if there is no config context data config_context_data = self.config_context_data or [] diff --git a/netbox/extras/models/mixins.py b/netbox/extras/models/mixins.py index cb1d31837..0950324c8 100644 --- a/netbox/extras/models/mixins.py +++ b/netbox/extras/models/mixins.py @@ -8,6 +8,16 @@ __all__ = ( class PythonModuleMixin: + def get_jobs(self, name): + """ + Returns a list of Jobs associated with this specific script or report module + :param name: The class name of the script or report + :return: List of Jobs associated with this + """ + return self.jobs.filter( + name=name + ) + @property def path(self): return os.path.splitext(self.file_path)[0] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 778d7b68d..4ac36a3ac 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -771,7 +771,7 @@ class Bookmark(models.Model): ) user = models.ForeignKey( to=settings.AUTH_USER_MODEL, - on_delete=models.PROTECT + on_delete=models.CASCADE ) objects = RestrictedQuerySet.as_manager() diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 478dedf92..b3bdc0a3e 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -120,34 +120,29 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): if self.model._meta.model_name == 'device': base_query.add((Q(locations=OuterRef('location')) | Q(locations=None)), Q.AND) base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND) - base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND) - base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND) - region_field = 'site__region' - sitegroup_field = 'site__group' elif self.model._meta.model_name == 'virtualmachine': - base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND) - base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND) base_query.add(Q(device_types=None), Q.AND) - region_field = 'cluster__site__region' - sitegroup_field = 'cluster__site__group' + + base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND) + base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND) base_query.add( (Q( - regions__tree_id=OuterRef(f'{region_field}__tree_id'), - regions__level__lte=OuterRef(f'{region_field}__level'), - regions__lft__lte=OuterRef(f'{region_field}__lft'), - regions__rght__gte=OuterRef(f'{region_field}__rght'), + regions__tree_id=OuterRef('site__region__tree_id'), + regions__level__lte=OuterRef('site__region__level'), + regions__lft__lte=OuterRef('site__region__lft'), + regions__rght__gte=OuterRef('site__region__rght'), ) | Q(regions=None)), Q.AND ) base_query.add( (Q( - site_groups__tree_id=OuterRef(f'{sitegroup_field}__tree_id'), - site_groups__level__lte=OuterRef(f'{sitegroup_field}__level'), - site_groups__lft__lte=OuterRef(f'{sitegroup_field}__lft'), - site_groups__rght__gte=OuterRef(f'{sitegroup_field}__rght'), + site_groups__tree_id=OuterRef('site__group__tree_id'), + site_groups__level__lte=OuterRef('site__group__level'), + site_groups__lft__lte=OuterRef('site__group__lft'), + site_groups__rght__gte=OuterRef('site__group__rght'), ) | Q(site_groups=None)), Q.AND ) diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index 798a9f442..4c15e839a 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -68,21 +68,23 @@ def handle_changed_object(sender, instance, **kwargs): else: return - # Record an ObjectChange if applicable - if m2m_changed: - ObjectChange.objects.filter( + # Create/update an ObejctChange record for this change + objectchange = instance.to_objectchange(action) + # If this is a many-to-many field change, check for a previous ObjectChange instance recorded + # for this object by this request and update it + if m2m_changed and ( + prev_change := ObjectChange.objects.filter( changed_object_type=ContentType.objects.get_for_model(instance), changed_object_id=instance.pk, request_id=request.id - ).update( - postchange_data=instance.to_objectchange(action).postchange_data - ) - else: - objectchange = instance.to_objectchange(action) - if objectchange and objectchange.has_changes: - objectchange.user = request.user - objectchange.request_id = request.id - objectchange.save() + ).first() + ): + prev_change.postchange_data = objectchange.postchange_data + prev_change.save() + elif objectchange and objectchange.has_changes: + objectchange.user = request.user + objectchange.request_id = request.id + objectchange.save() # If this is an M2M change, update the previously queued webhook (from post_save) queue = events_queue.get() @@ -251,7 +253,8 @@ def process_job_start_event_rules(sender, **kwargs): Process event rules for jobs starting. """ event_rules = EventRule.objects.filter(type_job_start=True, enabled=True, content_types=sender.object_type) - process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_START, sender.data, sender.user.username) + username = sender.user.username if sender.user else None + process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_START, sender.data, username) @receiver(job_end) @@ -260,4 +263,5 @@ def process_job_end_event_rules(sender, **kwargs): Process event rules for jobs terminating. """ event_rules = EventRule.objects.filter(type_job_end=True, enabled=True, content_types=sender.object_type) - process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, sender.user.username) + username = sender.user.username if sender.user else None + process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, username) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 93be2d2c4..f40372a8f 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -14,7 +14,6 @@ from extras.reports import Report from extras.scripts import BooleanVar, IntegerVar, Script, StringVar from utilities.testing import APITestCase, APIViewTestCases - User = get_user_model() @@ -251,6 +250,23 @@ class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase): ) CustomFieldChoiceSet.objects.bulk_create(choice_sets) + def test_invalid_choice_items(self): + """ + Attempting to define each choice as a single-item list should return a 400 error. + """ + self.add_permissions('extras.add_customfieldchoiceset') + data = { + "name": "test", + "extra_choices": [ + ["choice1"], + ["choice2"], + ["choice3"], + ] + } + + response = self.client.post(self._get_list_url(), data, format='json', **self.header) + self.assertEqual(response.status_code, 400) + class CustomLinkTest(APIViewTestCases.APIViewTestCase): model = CustomLink diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index ef9398401..cb3f08acb 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -270,7 +270,12 @@ class ConfigContextTest(TestCase): tag = Tag.objects.first() cluster_type = ClusterType.objects.create(name="Cluster Type") cluster_group = ClusterGroup.objects.create(name="Cluster Group") - cluster = Cluster.objects.create(name="Cluster", group=cluster_group, type=cluster_type) + cluster = Cluster.objects.create( + name="Cluster", + group=cluster_group, + type=cluster_type, + site=site, + ) region_context = ConfigContext.objects.create( name="region", @@ -354,6 +359,41 @@ class ConfigContextTest(TestCase): annotated_queryset = VirtualMachine.objects.filter(name=virtual_machine.name).annotate_config_context_data() self.assertEqual(virtual_machine.get_config_context(), annotated_queryset[0].get_config_context()) + def test_virtualmachine_site_context(self): + """ + Check that config context associated with a site applies to a VM whether the VM is assigned + directly to that site or via its cluster. + """ + site = Site.objects.first() + cluster_type = ClusterType.objects.create(name="Cluster Type") + cluster = Cluster.objects.create(name="Cluster", type=cluster_type, site=site) + vm_role = DeviceRole.objects.first() + + # Create a ConfigContext associated with the site + context = ConfigContext.objects.create( + name="context1", + weight=100, + data={"foo": True} + ) + context.sites.add(site) + + # Create one VM assigned directly to the site, and one assigned via the cluster + vm1 = VirtualMachine.objects.create(name="VM 1", site=site, role=vm_role) + vm2 = VirtualMachine.objects.create(name="VM 2", cluster=cluster, role=vm_role) + + # Check that their individually-rendered config contexts are identical + self.assertEqual( + vm1.get_config_context(), + vm2.get_config_context() + ) + + # Check that their annotated config contexts are identical + vms = VirtualMachine.objects.filter(pk__in=(vm1.pk, vm2.pk)).annotate_config_context_data() + self.assertEqual( + vms[0].get_config_context(), + vms[1].get_config_context() + ) + def test_multiple_tags_return_distinct_objects(self): """ Tagged items use a generic relationship, which results in duplicate rows being returned when queried. diff --git a/netbox/extras/views.py b/netbox/extras/views.py index a3dd7f193..ad00d7412 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -18,7 +18,6 @@ from extras.dashboard.utils import get_widget_class from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.views import generic from utilities.forms import ConfirmationForm, get_field_value -from utilities.htmx import is_htmx from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.rqworker import get_workers_for_queue from utilities.templatetags.builtins.filters import render_markdown @@ -890,7 +889,7 @@ class DashboardWidgetAddView(LoginRequiredMixin, View): template_name = 'extras/dashboard/widget_add.html' def get(self, request): - if not is_htmx(request): + if not request.htmx: return redirect('home') initial = request.GET or { @@ -940,7 +939,7 @@ class DashboardWidgetConfigView(LoginRequiredMixin, View): template_name = 'extras/dashboard/widget_config.html' def get(self, request, id): - if not is_htmx(request): + if not request.htmx: return redirect('home') widget = request.user.dashboard.get_widget(id) @@ -981,7 +980,7 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View): template_name = 'generic/object_delete.html' def get(self, request, id): - if not is_htmx(request): + if not request.htmx: return redirect('home') widget = request.user.dashboard.get_widget(id) @@ -1057,16 +1056,14 @@ class ReportView(ContentTypePermissionRequiredMixin, View): def get(self, request, module, name): module = get_report_module(module, request) report = module.reports[name]() + jobs = module.get_jobs(report.class_name) - object_type = ContentType.objects.get(app_label='extras', model='reportmodule') - report.result = Job.objects.filter( - object_type=object_type, - object_id=module.pk, - name=report.name, + report.result = jobs.filter( status__in=JobStatusChoices.TERMINAL_STATE_CHOICES ).first() return render(request, 'extras/report.html', { + 'job_count': jobs.count(), 'module': module, 'report': report, 'form': ReportForm(scheduling_enabled=report.scheduling_enabled), @@ -1078,6 +1075,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View): module = get_report_module(module, request) report = module.reports[name]() + jobs = module.get_jobs(report.class_name) form = ReportForm(request.POST, scheduling_enabled=report.scheduling_enabled) if form.is_valid(): @@ -1086,6 +1084,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View): if not get_workers_for_queue('default'): messages.error(request, "Unable to run report: RQ worker process not running.") return render(request, 'extras/report.html', { + 'job_count': jobs.count(), 'report': report, }) @@ -1103,6 +1102,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View): return redirect('extras:report_result', job_pk=job.pk) return render(request, 'extras/report.html', { + 'job_count': jobs.count(), 'module': module, 'report': report, 'form': form, @@ -1117,8 +1117,10 @@ class ReportSourceView(ContentTypePermissionRequiredMixin, View): def get(self, request, module, name): module = get_report_module(module, request) report = module.reports[name]() + jobs = module.get_jobs(report.class_name) return render(request, 'extras/report/source.html', { + 'job_count': jobs.count(), 'module': module, 'report': report, 'tab': 'source', @@ -1133,13 +1135,7 @@ class ReportJobsView(ContentTypePermissionRequiredMixin, View): def get(self, request, module, name): module = get_report_module(module, request) report = module.reports[name]() - - object_type = ContentType.objects.get(app_label='extras', model='reportmodule') - jobs = Job.objects.filter( - object_type=object_type, - object_id=module.pk, - name=report.class_name - ) + jobs = module.get_jobs(report.class_name) jobs_table = JobTable( data=jobs, @@ -1149,6 +1145,7 @@ class ReportJobsView(ContentTypePermissionRequiredMixin, View): jobs_table.configure(request) return render(request, 'extras/report/jobs.html', { + 'job_count': jobs.count(), 'module': module, 'report': report, 'table': jobs_table, @@ -1171,7 +1168,7 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View): report = module.reports[job.name] # If this is an HTMX request, return only the result HTML - if is_htmx(request): + if request.htmx: response = render(request, 'extras/htmx/report_result.html', { 'report': report, 'job': job, @@ -1232,19 +1229,11 @@ class ScriptView(ContentTypePermissionRequiredMixin, View): def get(self, request, module, name): module = get_script_module(module, request) script = module.scripts[name]() + jobs = module.get_jobs(script.class_name) form = script.as_form(initial=normalize_querydict(request.GET)) - # Look for a pending Job (use the latest one by creation timestamp) - object_type = ContentType.objects.get(app_label='extras', model='scriptmodule') - script.result = Job.objects.filter( - object_type=object_type, - object_id=module.pk, - name=script.name, - ).exclude( - status__in=JobStatusChoices.TERMINAL_STATE_CHOICES - ).first() - return render(request, 'extras/script.html', { + 'job_count': jobs.count(), 'module': module, 'script': script, 'form': form, @@ -1256,6 +1245,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, View): module = get_script_module(module, request) script = module.scripts[name]() + jobs = module.get_jobs(script.class_name) form = script.as_form(request.POST, request.FILES) # Allow execution only if RQ worker process is running @@ -1279,6 +1269,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, View): return redirect('extras:script_result', job_pk=job.pk) return render(request, 'extras/script.html', { + 'job_count': jobs.count(), 'module': module, 'script': script, 'form': form, @@ -1293,8 +1284,10 @@ class ScriptSourceView(ContentTypePermissionRequiredMixin, View): def get(self, request, module, name): module = get_script_module(module, request) script = module.scripts[name]() + jobs = module.get_jobs(script.class_name) return render(request, 'extras/script/source.html', { + 'job_count': jobs.count(), 'module': module, 'script': script, 'tab': 'source', @@ -1309,13 +1302,7 @@ class ScriptJobsView(ContentTypePermissionRequiredMixin, View): def get(self, request, module, name): module = get_script_module(module, request) script = module.scripts[name]() - - object_type = ContentType.objects.get(app_label='extras', model='scriptmodule') - jobs = Job.objects.filter( - object_type=object_type, - object_id=module.pk, - name=script.class_name - ) + jobs = module.get_jobs(script.class_name) jobs_table = JobTable( data=jobs, @@ -1325,6 +1312,7 @@ class ScriptJobsView(ContentTypePermissionRequiredMixin, View): jobs_table.configure(request) return render(request, 'extras/script/jobs.html', { + 'job_count': jobs.count(), 'module': module, 'script': script, 'table': jobs_table, @@ -1345,7 +1333,7 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, View): script = module.scripts[job.name]() # If this is an HTMX request, return only the result HTML - if is_htmx(request): + if request.htmx: response = render(request, 'extras/htmx/script_result.html', { 'script': script, 'job': job, diff --git a/netbox/ipam/forms/bulk_create.py b/netbox/ipam/forms/bulk_create.py index ea553c655..856476786 100644 --- a/netbox/ipam/forms/bulk_create.py +++ b/netbox/ipam/forms/bulk_create.py @@ -1,7 +1,6 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from utilities.forms import BootstrapMixin from utilities.forms.fields import ExpandableIPAddressField __all__ = ( @@ -9,7 +8,7 @@ __all__ = ( ) -class IPAddressBulkCreateForm(BootstrapMixin, forms.Form): +class IPAddressBulkCreateForm(forms.Form): pattern = ExpandableIPAddressField( label=_('Address pattern') ) diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index bf4825be9..72d57e941 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -254,7 +254,7 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm): mark_utilized = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect(), - label=_('Treat as 100% utilized') + label=_('Treat as fully utilized') ) description = forms.CharField( label=_('Description'), @@ -298,7 +298,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): mark_utilized = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect(), - label=_('Treat as 100% utilized') + label=_('Treat as fully utilized') ) description = forms.CharField( label=_('Description'), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index e6acdb012..909de886f 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -240,7 +240,7 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): ) mark_utilized = forms.NullBooleanField( required=False, - label=_('Marked as 100% utilized'), + label=_('Treat as fully utilized'), widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) @@ -279,7 +279,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): ) mark_utilized = forms.NullBooleanField( required=False, - label=_('Marked as 100% utilized'), + label=_('Treat as fully utilized'), widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 6c445ef27..f6a67d643 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -11,7 +11,7 @@ from ipam.models import * from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.exceptions import PermissionsViolation -from utilities.forms import BootstrapMixin, add_blank_choice +from utilities.forms import add_blank_choice from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, @@ -214,7 +214,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm): required=False, selector=True, query_params={ - 'site_id': '$site', + 'available_at_site': '$site', }, label=_('VLAN'), ) @@ -419,7 +419,7 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm): ] -class IPAddressAssignForm(BootstrapMixin, forms.Form): +class IPAddressAssignForm(forms.Form): vrf_id = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -504,7 +504,7 @@ class FHRPGroupForm(NetBoxModelForm): }) -class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm): +class FHRPGroupAssignmentForm(forms.ModelForm): group = DynamicModelChoiceField( label=_('Group'), queryset=FHRPGroup.objects.all() @@ -738,7 +738,6 @@ class ServiceCreateForm(ServiceForm): # Fields which may be populated from a ServiceTemplate are not required for field in ('name', 'protocol', 'ports'): self.fields[field].required = False - del self.fields[field].widget.attrs['required'] def clean(self): super().clean() diff --git a/netbox/ipam/migrations/0047_prefix_depth_children.py b/netbox/ipam/migrations/0047_prefix_depth_children.py deleted file mode 100644 index 4c49b1358..000000000 --- a/netbox/ipam/migrations/0047_prefix_depth_children.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0046_set_vlangroup_scope_types'), - ] - - operations = [ - migrations.AddField( - model_name='prefix', - name='_children', - field=models.PositiveBigIntegerField(default=0, editable=False), - ), - migrations.AddField( - model_name='prefix', - name='_depth', - field=models.PositiveSmallIntegerField(default=0, editable=False), - ), - ] diff --git a/netbox/ipam/migrations/0047_squashed_0053.py b/netbox/ipam/migrations/0047_squashed_0053.py new file mode 100644 index 000000000..470261316 --- /dev/null +++ b/netbox/ipam/migrations/0047_squashed_0053.py @@ -0,0 +1,143 @@ +import django.core.validators +import django.db.models.deletion +import taggit.managers +from django.db import migrations, models + +import ipam.fields +import utilities.json + + +class Migration(migrations.Migration): + + replaces = [ + ('ipam', '0047_prefix_depth_children'), + ('ipam', '0048_prefix_populate_depth_children'), + ('ipam', '0049_prefix_mark_utilized'), + ('ipam', '0050_iprange'), + ('ipam', '0051_extend_tag_support'), + ('ipam', '0052_fhrpgroup'), + ('ipam', '0053_asn_model') + ] + + dependencies = [ + ('ipam', '0046_set_vlangroup_scope_types'), + ('tenancy', '0001_squashed_0012'), + ('extras', '0002_squashed_0059'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.AddField( + model_name='prefix', + name='_children', + field=models.PositiveBigIntegerField(default=0, editable=False), + ), + migrations.AddField( + model_name='prefix', + name='_depth', + field=models.PositiveSmallIntegerField(default=0, editable=False), + ), + migrations.AddField( + model_name='prefix', + name='mark_utilized', + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name='IPRange', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('start_address', ipam.fields.IPAddressField()), + ('end_address', ipam.fields.IPAddressField()), + ('size', models.PositiveIntegerField(editable=False)), + ('status', models.CharField(default='active', max_length=50)), + ('description', models.CharField(blank=True, max_length=200)), + ('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ip_ranges', to='ipam.role')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_ranges', to='tenancy.tenant')), + ('vrf', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_ranges', to='ipam.vrf')), + ], + options={ + 'verbose_name': 'IP range', + 'verbose_name_plural': 'IP ranges', + 'ordering': (models.OrderBy(models.F('vrf'), nulls_first=True), 'start_address', 'pk'), + }, + ), + migrations.AddField( + model_name='rir', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='role', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='vlangroup', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.CreateModel( + name='FHRPGroup', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('group_id', models.PositiveSmallIntegerField()), + ('protocol', models.CharField(max_length=50)), + ('auth_type', models.CharField(blank=True, max_length=50)), + ('auth_key', models.CharField(blank=True, max_length=255)), + ('description', models.CharField(blank=True, max_length=200)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'verbose_name': 'FHRP group', + 'ordering': ['protocol', 'group_id', 'pk'], + }, + ), + migrations.AlterField( + model_name='ipaddress', + name='assigned_object_type', + field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'fhrpgroup')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'), + ), + migrations.CreateModel( + name='FHRPGroupAssignment', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('interface_id', models.PositiveIntegerField()), + ('priority', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(255)])), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ipam.fhrpgroup')), + ('interface_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ], + options={ + 'verbose_name': 'FHRP group assignment', + 'ordering': ('-priority', 'pk'), + 'unique_together': {('interface_type', 'interface_id', 'group')}, + }, + ), + migrations.CreateModel( + name='ASN', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('asn', ipam.fields.ASNField(unique=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('rir', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='asns', to='ipam.rir')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='asns', to='tenancy.tenant')), + ], + options={ + 'verbose_name': 'ASN', + 'verbose_name_plural': 'ASNs', + 'ordering': ['asn'], + }, + ), + ] diff --git a/netbox/ipam/migrations/0048_prefix_populate_depth_children.py b/netbox/ipam/migrations/0048_prefix_populate_depth_children.py deleted file mode 100644 index 5ec448ee1..000000000 --- a/netbox/ipam/migrations/0048_prefix_populate_depth_children.py +++ /dev/null @@ -1,37 +0,0 @@ -import sys -from django.db import migrations - -from ipam.utils import rebuild_prefixes - - -def populate_prefix_hierarchy(apps, schema_editor): - """ - Populate _depth and _children attrs for all Prefixes. - """ - Prefix = apps.get_model('ipam', 'Prefix') - VRF = apps.get_model('ipam', 'VRF') - - total_count = Prefix.objects.count() - if 'test' not in sys.argv: - print(f'\nUpdating {total_count} prefixes...') - - # Rebuild the global table - rebuild_prefixes(None) - - # Iterate through all VRFs, rebuilding each - for vrf in VRF.objects.all(): - rebuild_prefixes(vrf.pk) - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0047_prefix_depth_children'), - ] - - operations = [ - migrations.RunPython( - code=populate_prefix_hierarchy, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/ipam/migrations/0049_prefix_mark_utilized.py b/netbox/ipam/migrations/0049_prefix_mark_utilized.py deleted file mode 100644 index 4274d92a0..000000000 --- a/netbox/ipam/migrations/0049_prefix_mark_utilized.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0048_prefix_populate_depth_children'), - ] - - operations = [ - migrations.AddField( - model_name='prefix', - name='mark_utilized', - field=models.BooleanField(default=False), - ), - ] diff --git a/netbox/ipam/migrations/0050_iprange.py b/netbox/ipam/migrations/0050_iprange.py deleted file mode 100644 index 374b2547c..000000000 --- a/netbox/ipam/migrations/0050_iprange.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 3.2.5 on 2021-07-16 14:15 - -from utilities.json import CustomFieldJSONEncoder -from django.db import migrations, models -import django.db.models.deletion -import django.db.models.expressions -import ipam.fields -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0061_extras_change_logging'), - ('tenancy', '0001_squashed_0012'), - ('ipam', '0049_prefix_mark_utilized'), - ] - - operations = [ - migrations.CreateModel( - name='IPRange', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('start_address', ipam.fields.IPAddressField()), - ('end_address', ipam.fields.IPAddressField()), - ('size', models.PositiveIntegerField(editable=False)), - ('status', models.CharField(default='active', max_length=50)), - ('description', models.CharField(blank=True, max_length=200)), - ('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ip_ranges', to='ipam.role')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_ranges', to='tenancy.tenant')), - ('vrf', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_ranges', to='ipam.vrf')), - ], - options={ - 'verbose_name': 'IP range', - 'verbose_name_plural': 'IP ranges', - 'ordering': (django.db.models.expressions.OrderBy(django.db.models.expressions.F('vrf'), nulls_first=True), 'start_address', 'pk'), - }, - ), - ] diff --git a/netbox/ipam/migrations/0051_extend_tag_support.py b/netbox/ipam/migrations/0051_extend_tag_support.py deleted file mode 100644 index ea31a6645..000000000 --- a/netbox/ipam/migrations/0051_extend_tag_support.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-21 14:50 - -from django.db import migrations -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0062_clear_secrets_changelog'), - ('ipam', '0050_iprange'), - ] - - operations = [ - migrations.AddField( - model_name='rir', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - migrations.AddField( - model_name='role', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - migrations.AddField( - model_name='vlangroup', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - ] diff --git a/netbox/ipam/migrations/0052_fhrpgroup.py b/netbox/ipam/migrations/0052_fhrpgroup.py deleted file mode 100644 index e69e49d48..000000000 --- a/netbox/ipam/migrations/0052_fhrpgroup.py +++ /dev/null @@ -1,58 +0,0 @@ -from utilities.json import CustomFieldJSONEncoder -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('extras', '0064_configrevision'), - ('ipam', '0051_extend_tag_support'), - ] - - operations = [ - migrations.CreateModel( - name='FHRPGroup', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('group_id', models.PositiveSmallIntegerField()), - ('protocol', models.CharField(max_length=50)), - ('auth_type', models.CharField(blank=True, max_length=50)), - ('auth_key', models.CharField(blank=True, max_length=255)), - ('description', models.CharField(blank=True, max_length=200)), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ], - options={ - 'verbose_name': 'FHRP group', - 'ordering': ['protocol', 'group_id', 'pk'], - }, - ), - migrations.AlterField( - model_name='ipaddress', - name='assigned_object_type', - field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'fhrpgroup')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'), - ), - migrations.CreateModel( - name='FHRPGroupAssignment', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('interface_id', models.PositiveIntegerField()), - ('priority', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(255)])), - ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ipam.fhrpgroup')), - ('interface_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), - ], - options={ - 'verbose_name': 'FHRP group assignment', - 'ordering': ('-priority', 'pk'), - 'unique_together': {('interface_type', 'interface_id', 'group')}, - }, - ), - ] diff --git a/netbox/ipam/migrations/0053_asn_model.py b/netbox/ipam/migrations/0053_asn_model.py deleted file mode 100644 index 99bde12e6..000000000 --- a/netbox/ipam/migrations/0053_asn_model.py +++ /dev/null @@ -1,36 +0,0 @@ -import ipam.fields -from utilities.json import CustomFieldJSONEncoder -from django.db import migrations, models -import django.db.models.deletion -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('tenancy', '0004_extend_tag_support'), - ('extras', '0064_configrevision'), - ('ipam', '0052_fhrpgroup'), - ] - - operations = [ - migrations.CreateModel( - name='ASN', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('asn', ipam.fields.ASNField(unique=True)), - ('description', models.CharField(blank=True, max_length=200)), - ('rir', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='asns', to='ipam.rir')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='asns', to='tenancy.tenant')), - ], - options={ - 'verbose_name': 'ASN', - 'verbose_name_plural': 'ASNs', - 'ordering': ['asn'], - }, - ), - ] diff --git a/netbox/ipam/migrations/0054_squashed_0067.py b/netbox/ipam/migrations/0054_squashed_0067.py new file mode 100644 index 000000000..40073ca29 --- /dev/null +++ b/netbox/ipam/migrations/0054_squashed_0067.py @@ -0,0 +1,393 @@ +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.functions.comparison +import taggit.managers +from django.db import migrations, models + +import ipam.fields +import ipam.lookups +import utilities.json + + +class Migration(migrations.Migration): + + replaces = [ + ('ipam', '0054_vlangroup_min_max_vids'), + ('ipam', '0055_servicetemplate'), + ('ipam', '0056_standardize_id_fields'), + ('ipam', '0057_created_datetimefield'), + ('ipam', '0058_ipaddress_nat_inside_nonunique'), + ('ipam', '0059_l2vpn'), + ('ipam', '0060_alter_l2vpn_slug'), + ('ipam', '0061_fhrpgroup_name'), + ('ipam', '0062_unique_constraints'), + ('ipam', '0063_standardize_description_comments'), + ('ipam', '0064_clear_search_cache'), + ('ipam', '0065_asnrange'), + ('ipam', '0066_iprange_mark_utilized'), + ('ipam', '0067_ipaddress_index_host') + ] + + dependencies = [ + ('tenancy', '0007_contact_link'), + ('contenttypes', '0002_remove_content_type_name'), + ('extras', '0060_squashed_0086'), + ('ipam', '0053_asn_model'), + ('tenancy', '0009_standardize_description_comments'), + ] + + operations = [ + migrations.AddField( + model_name='vlangroup', + name='max_vid', + field=models.PositiveSmallIntegerField(default=4094, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)]), + ), + migrations.AddField( + model_name='vlangroup', + name='min_vid', + field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)]), + ), + migrations.AlterField( + model_name='aggregate', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='asn', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='fhrpgroup', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='fhrpgroupassignment', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='ipaddress', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='iprange', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='prefix', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='rir', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='role', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='routetarget', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='service', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='vlan', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='vlangroup', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='vrf', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='fhrpgroupassignment', + name='interface_id', + field=models.PositiveBigIntegerField(), + ), + migrations.AlterField( + model_name='ipaddress', + name='assigned_object_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='aggregate', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='asn', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='fhrpgroup', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='fhrpgroupassignment', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='ipaddress', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='iprange', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='prefix', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='rir', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='role', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='routetarget', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='service', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.CreateModel( + name='ServiceTemplate', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('protocol', models.CharField(max_length=50)), + ('ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)]), size=None)), + ('description', models.CharField(blank=True, max_length=200)), + ('name', models.CharField(max_length=100, unique=True)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.AlterField( + model_name='vlan', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='vlangroup', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='vrf', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='ipaddress', + name='nat_inside', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.ipaddress'), + ), + migrations.CreateModel( + name='L2VPN', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(max_length=100, unique=True)), + ('type', models.CharField(max_length=50)), + ('identifier', models.BigIntegerField(blank=True, null=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('export_targets', models.ManyToManyField(blank=True, related_name='exporting_l2vpns', to='ipam.routetarget')), + ('import_targets', models.ManyToManyField(blank=True, related_name='importing_l2vpns', to='ipam.routetarget')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='l2vpns', to='tenancy.tenant')), + ], + options={ + 'verbose_name': 'L2VPN', + 'ordering': ('name', 'identifier'), + }, + ), + migrations.CreateModel( + name='L2VPNTermination', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('assigned_object_id', models.PositiveBigIntegerField()), + ('assigned_object_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'vlan')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), + ('l2vpn', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='ipam.l2vpn')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'verbose_name': 'L2VPN termination', + 'ordering': ('l2vpn',), + }, + ), + migrations.AddConstraint( + model_name='l2vpntermination', + constraint=models.UniqueConstraint(fields=('assigned_object_type', 'assigned_object_id'), name='ipam_l2vpntermination_assigned_object'), + ), + migrations.AddField( + model_name='fhrpgroup', + name='name', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AlterUniqueTogether( + name='fhrpgroupassignment', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='vlan', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='vlangroup', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='fhrpgroupassignment', + constraint=models.UniqueConstraint(fields=('interface_type', 'interface_id', 'group'), name='ipam_fhrpgroupassignment_unique_interface_group'), + ), + migrations.AddConstraint( + model_name='vlan', + constraint=models.UniqueConstraint(fields=('group', 'vid'), name='ipam_vlan_unique_group_vid'), + ), + migrations.AddConstraint( + model_name='vlan', + constraint=models.UniqueConstraint(fields=('group', 'name'), name='ipam_vlan_unique_group_name'), + ), + migrations.AddConstraint( + model_name='vlangroup', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name'), name='ipam_vlangroup_unique_scope_name'), + ), + migrations.AddConstraint( + model_name='vlangroup', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'slug'), name='ipam_vlangroup_unique_scope_slug'), + ), + migrations.AddField( + model_name='aggregate', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='asn', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='fhrpgroup', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='ipaddress', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='iprange', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='l2vpn', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='prefix', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='routetarget', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='service', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='servicetemplate', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='vlan', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='vrf', + name='comments', + field=models.TextField(blank=True), + ), + migrations.CreateModel( + name='ASNRange', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('description', models.CharField(blank=True, max_length=200)), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(max_length=100, unique=True)), + ('start', ipam.fields.ASNField()), + ('end', ipam.fields.ASNField()), + ('rir', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='asn_ranges', to='ipam.rir')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='asn_ranges', to='tenancy.tenant')), + ], + options={ + 'verbose_name': 'ASN range', + 'verbose_name_plural': 'ASN ranges', + 'ordering': ('name',), + }, + ), + migrations.AddField( + model_name='iprange', + name='mark_utilized', + field=models.BooleanField(default=False), + ), + migrations.AddIndex( + model_name='ipaddress', + index=models.Index(django.db.models.functions.comparison.Cast(ipam.lookups.Host('address'), output_field=ipam.fields.IPAddressField()), name='ipam_ipaddress_host'), + ), + ] diff --git a/netbox/ipam/migrations/0054_vlangroup_min_max_vids.py b/netbox/ipam/migrations/0054_vlangroup_min_max_vids.py deleted file mode 100644 index 7b901fe13..000000000 --- a/netbox/ipam/migrations/0054_vlangroup_min_max_vids.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 3.2.10 on 2021-12-23 15:24 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0145_site_remove_deprecated_fields'), - ('ipam', '0053_asn_model'), - ] - - operations = [ - migrations.AddField( - model_name='vlangroup', - name='max_vid', - field=models.PositiveSmallIntegerField(default=4094, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)]), - ), - migrations.AddField( - model_name='vlangroup', - name='min_vid', - field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)]), - ), - ] diff --git a/netbox/ipam/migrations/0055_servicetemplate.py b/netbox/ipam/migrations/0055_servicetemplate.py deleted file mode 100644 index c8ba6645c..000000000 --- a/netbox/ipam/migrations/0055_servicetemplate.py +++ /dev/null @@ -1,33 +0,0 @@ -import django.contrib.postgres.fields -from utilities.json import CustomFieldJSONEncoder -import django.core.validators -from django.db import migrations, models -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0070_customlink_enabled'), - ('ipam', '0054_vlangroup_min_max_vids'), - ] - - operations = [ - migrations.CreateModel( - name='ServiceTemplate', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('protocol', models.CharField(max_length=50)), - ('ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)]), size=None)), - ('description', models.CharField(blank=True, max_length=200)), - ('name', models.CharField(max_length=100, unique=True)), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ], - options={ - 'ordering': ('name',), - }, - ), - ] diff --git a/netbox/ipam/migrations/0056_standardize_id_fields.py b/netbox/ipam/migrations/0056_standardize_id_fields.py deleted file mode 100644 index cb7564450..000000000 --- a/netbox/ipam/migrations/0056_standardize_id_fields.py +++ /dev/null @@ -1,99 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0055_servicetemplate'), - ] - - operations = [ - # Model IDs - migrations.AlterField( - model_name='aggregate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='asn', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='fhrpgroup', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='fhrpgroupassignment', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='ipaddress', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='iprange', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='prefix', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='rir', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='role', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='routetarget', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='service', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='servicetemplate', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='vlan', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='vlangroup', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='vrf', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), - ), - - # GFK IDs - migrations.AlterField( - model_name='fhrpgroupassignment', - name='interface_id', - field=models.PositiveBigIntegerField(), - ), - migrations.AlterField( - model_name='ipaddress', - name='assigned_object_id', - field=models.PositiveBigIntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/ipam/migrations/0057_created_datetimefield.py b/netbox/ipam/migrations/0057_created_datetimefield.py deleted file mode 100644 index f2ca7ab95..000000000 --- a/netbox/ipam/migrations/0057_created_datetimefield.py +++ /dev/null @@ -1,88 +0,0 @@ -# Generated by Django 4.0.2 on 2022-02-08 18:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0056_standardize_id_fields'), - ] - - operations = [ - migrations.AlterField( - model_name='aggregate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='asn', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='fhrpgroup', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='fhrpgroupassignment', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='ipaddress', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='iprange', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='prefix', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='rir', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='role', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='routetarget', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='service', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='servicetemplate', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='vlan', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='vlangroup', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='vrf', - name='created', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - ] diff --git a/netbox/ipam/migrations/0059_l2vpn.py b/netbox/ipam/migrations/0059_l2vpn.py deleted file mode 100644 index 59dbab632..000000000 --- a/netbox/ipam/migrations/0059_l2vpn.py +++ /dev/null @@ -1,60 +0,0 @@ -from utilities.json import CustomFieldJSONEncoder -from django.db import migrations, models -import django.db.models.deletion -import taggit.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0075_configcontext_locations'), - ('contenttypes', '0002_remove_content_type_name'), - ('tenancy', '0007_contact_link'), - ('ipam', '0058_ipaddress_nat_inside_nonunique'), - ] - - operations = [ - migrations.CreateModel( - name='L2VPN', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('name', models.CharField(max_length=100, unique=True)), - ('slug', models.SlugField()), - ('type', models.CharField(max_length=50)), - ('identifier', models.BigIntegerField(blank=True, null=True)), - ('description', models.CharField(blank=True, max_length=200)), - ('export_targets', models.ManyToManyField(blank=True, related_name='exporting_l2vpns', to='ipam.routetarget')), - ('import_targets', models.ManyToManyField(blank=True, related_name='importing_l2vpns', to='ipam.routetarget')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='l2vpns', to='tenancy.tenant')), - ], - options={ - 'verbose_name': 'L2VPN', - 'ordering': ('name', 'identifier'), - }, - ), - migrations.CreateModel( - name='L2VPNTermination', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)), - ('assigned_object_id', models.PositiveBigIntegerField()), - ('assigned_object_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'vlan')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), - ('l2vpn', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='ipam.l2vpn')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ], - options={ - 'verbose_name': 'L2VPN termination', - 'ordering': ('l2vpn',), - }, - ), - migrations.AddConstraint( - model_name='l2vpntermination', - constraint=models.UniqueConstraint(fields=('assigned_object_type', 'assigned_object_id'), name='ipam_l2vpntermination_assigned_object'), - ), - ] diff --git a/netbox/ipam/migrations/0060_alter_l2vpn_slug.py b/netbox/ipam/migrations/0060_alter_l2vpn_slug.py deleted file mode 100644 index 9e70c2063..000000000 --- a/netbox/ipam/migrations/0060_alter_l2vpn_slug.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.7 on 2022-08-22 15:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0059_l2vpn'), - ] - - operations = [ - migrations.AlterField( - model_name='l2vpn', - name='slug', - field=models.SlugField(max_length=100, unique=True), - ), - ] diff --git a/netbox/ipam/migrations/0061_fhrpgroup_name.py b/netbox/ipam/migrations/0061_fhrpgroup_name.py deleted file mode 100644 index 7e232c18f..000000000 --- a/netbox/ipam/migrations/0061_fhrpgroup_name.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.7 on 2022-09-20 23:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0060_alter_l2vpn_slug'), - ] - - operations = [ - migrations.AddField( - model_name='fhrpgroup', - name='name', - field=models.CharField(blank=True, max_length=100), - ), - ] diff --git a/netbox/ipam/migrations/0062_unique_constraints.py b/netbox/ipam/migrations/0062_unique_constraints.py deleted file mode 100644 index 47c1a1214..000000000 --- a/netbox/ipam/migrations/0062_unique_constraints.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0061_fhrpgroup_name'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='fhrpgroupassignment', - unique_together=set(), - ), - migrations.AlterUniqueTogether( - name='vlan', - unique_together=set(), - ), - migrations.AlterUniqueTogether( - name='vlangroup', - unique_together=set(), - ), - migrations.AddConstraint( - model_name='fhrpgroupassignment', - constraint=models.UniqueConstraint(fields=('interface_type', 'interface_id', 'group'), name='ipam_fhrpgroupassignment_unique_interface_group'), - ), - migrations.AddConstraint( - model_name='vlan', - constraint=models.UniqueConstraint(fields=('group', 'vid'), name='ipam_vlan_unique_group_vid'), - ), - migrations.AddConstraint( - model_name='vlan', - constraint=models.UniqueConstraint(fields=('group', 'name'), name='ipam_vlan_unique_group_name'), - ), - migrations.AddConstraint( - model_name='vlangroup', - constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name'), name='ipam_vlangroup_unique_scope_name'), - ), - migrations.AddConstraint( - model_name='vlangroup', - constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'slug'), name='ipam_vlangroup_unique_scope_slug'), - ), - ] diff --git a/netbox/ipam/migrations/0063_standardize_description_comments.py b/netbox/ipam/migrations/0063_standardize_description_comments.py deleted file mode 100644 index 3a4959d14..000000000 --- a/netbox/ipam/migrations/0063_standardize_description_comments.py +++ /dev/null @@ -1,73 +0,0 @@ -# Generated by Django 4.1.2 on 2022-11-03 18:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0062_unique_constraints'), - ] - - operations = [ - migrations.AddField( - model_name='aggregate', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='asn', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='fhrpgroup', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='ipaddress', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='iprange', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='l2vpn', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='prefix', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='routetarget', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='service', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='servicetemplate', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='vlan', - name='comments', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='vrf', - name='comments', - field=models.TextField(blank=True), - ), - ] diff --git a/netbox/ipam/migrations/0064_clear_search_cache.py b/netbox/ipam/migrations/0064_clear_search_cache.py deleted file mode 100644 index 856fe99e1..000000000 --- a/netbox/ipam/migrations/0064_clear_search_cache.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import migrations - - -def clear_cache(apps, schema_editor): - """ - Clear existing CachedValues referencing IPAddressFields or IPNetworkFields. (#11658 - introduced new cache record types for these.) - """ - ContentType = apps.get_model('contenttypes', 'ContentType') - CachedValue = apps.get_model('extras', 'CachedValue') - - for model_name in ('Aggregate', 'IPAddress', 'IPRange', 'Prefix'): - try: - content_type = ContentType.objects.get(app_label='ipam', model=model_name.lower()) - CachedValue.objects.filter(object_type=content_type).delete() - except ContentType.DoesNotExist: - pass - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0063_standardize_description_comments'), - ] - - operations = [ - migrations.RunPython( - code=clear_cache, - reverse_code=migrations.RunPython.noop - ), - ] diff --git a/netbox/ipam/migrations/0065_asnrange.py b/netbox/ipam/migrations/0065_asnrange.py deleted file mode 100644 index 71d056dfd..000000000 --- a/netbox/ipam/migrations/0065_asnrange.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 4.1.7 on 2023-02-26 19:33 - -from django.db import migrations, models -import django.db.models.deletion -import ipam.fields -import taggit.managers -import utilities.json - - -class Migration(migrations.Migration): - - dependencies = [ - ('tenancy', '0009_standardize_description_comments'), - ('extras', '0087_dashboard'), - ('ipam', '0064_clear_search_cache'), - ] - - operations = [ - migrations.CreateModel( - name='ASNRange', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), - ('description', models.CharField(blank=True, max_length=200)), - ('name', models.CharField(max_length=100, unique=True)), - ('slug', models.SlugField(max_length=100, unique=True)), - ('start', ipam.fields.ASNField()), - ('end', ipam.fields.ASNField()), - ('rir', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='asn_ranges', to='ipam.rir')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='asn_ranges', to='tenancy.tenant')), - ], - options={ - 'verbose_name': 'ASN range', - 'verbose_name_plural': 'ASN ranges', - 'ordering': ('name',), - }, - ), - ] diff --git a/netbox/ipam/migrations/0066_iprange_mark_utilized.py b/netbox/ipam/migrations/0066_iprange_mark_utilized.py deleted file mode 100644 index 2489289fd..000000000 --- a/netbox/ipam/migrations/0066_iprange_mark_utilized.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.7 on 2023-02-28 14:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0065_asnrange'), - ] - - operations = [ - migrations.AddField( - model_name='iprange', - name='mark_utilized', - field=models.BooleanField(default=False), - ), - ] diff --git a/netbox/ipam/migrations/0067_ipaddress_index_host.py b/netbox/ipam/migrations/0067_ipaddress_index_host.py deleted file mode 100644 index 2383fd9aa..000000000 --- a/netbox/ipam/migrations/0067_ipaddress_index_host.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.1.10 on 2023-08-02 12:43 - -from django.db import migrations, models -import django.db.models.functions.comparison -import ipam.fields -import ipam.lookups - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0066_iprange_mark_utilized'), - ] - - operations = [ - migrations.AddIndex( - model_name='ipaddress', - index=models.Index(django.db.models.functions.comparison.Cast(ipam.lookups.Host('address'), output_field=ipam.fields.IPAddressField()), name='ipam_ipaddress_host'), - ), - ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 01e2ed1c7..76fae2990 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -268,7 +268,7 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel): mark_utilized = models.BooleanField( verbose_name=_('mark utilized'), default=False, - help_text=_("Treat as 100% utilized") + help_text=_("Treat as fully utilized") ) # Cached depth & child counts @@ -427,10 +427,10 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel): prefix = netaddr.IPSet(self.prefix) child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()]) - child_ranges = netaddr.IPSet() + child_ranges = [] for iprange in self.get_child_ranges(): - child_ranges.add(iprange.range) - available_ips = prefix - child_ips - child_ranges + child_ranges.append(iprange.range) + available_ips = prefix - child_ips - netaddr.IPSet(child_ranges) # IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31): @@ -535,7 +535,7 @@ class IPRange(PrimaryModel): mark_utilized = models.BooleanField( verbose_name=_('mark utilized'), default=False, - help_text=_("Treat as 100% utilized") + help_text=_("Treat as fully utilized") ) clone_fields = ( diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 3b50b257d..9e940ae9e 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -18,7 +18,7 @@ __all__ = ( 'RoleTable', ) -AVAILABLE_LABEL = mark_safe('Available') +AVAILABLE_LABEL = mark_safe('Available') AGGREGATE_COPY_BUTTON = """ {% copy_content record.pk prefix="aggregate_" %} diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index aee91e7d8..11de0381c 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -18,7 +18,7 @@ __all__ = ( 'VLANVirtualMachinesTable', ) -AVAILABLE_LABEL = mark_safe('Available') +AVAILABLE_LABEL = mark_safe('Available') VLAN_LINK = """ {% if record.pk %} diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 1598f0321..9c4a9a102 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -604,7 +604,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView): return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group') def prep_table_data(self, request, queryset, parent): - if not get_table_ordering(request, self.table): + if not request.GET.get('q') and not get_table_ordering(request, self.table): return add_available_ipaddresses(parent.prefix, queryset, parent.is_pool) return queryset @@ -1068,6 +1068,12 @@ class FHRPGroupAssignmentEditView(generic.ObjectEditView): instance.interface = get_object_or_404(content_type.model_class(), pk=request.GET.get('interface_id')) return instance + def get_extra_addanother_params(self, request): + return { + 'interface_type': request.GET.get('interface_type'), + 'interface_id': request.GET.get('interface_id'), + } + @register_model_view(FHRPGroupAssignment, 'delete') class FHRPGroupAssignmentDeleteView(generic.ObjectDeleteView): diff --git a/netbox/netbox/configuration_testing.py b/netbox/netbox/configuration_testing.py index cec05cabb..346cd89d2 100644 --- a/netbox/netbox/configuration_testing.py +++ b/netbox/netbox/configuration_testing.py @@ -39,6 +39,8 @@ REDIS = { SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' +DJANGO_ADMIN_ENABLED = True + DEFAULT_PERMISSIONS = {} LOGGING = { diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index faddf8c21..547e2079b 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -36,3 +36,7 @@ DEFAULT_ACTION_PERMISSIONS = { 'bulk_edit': {'change'}, 'bulk_delete': {'delete'}, } + +# General-purpose tokens +CENSOR_TOKEN = '********' +CENSOR_TOKEN_CHANGED = '***CHANGED***' diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py index 65460ebf1..fa82689a5 100644 --- a/netbox/netbox/forms/__init__.py +++ b/netbox/netbox/forms/__init__.py @@ -5,7 +5,6 @@ from django.utils.translation import gettext as _ from netbox.search import LookupTypes from netbox.search.backends import search_backend -from utilities.forms import BootstrapMixin from .base import * @@ -18,7 +17,7 @@ LOOKUP_CHOICES = ( ) -class SearchForm(BootstrapMixin, forms.Form): +class SearchForm(forms.Form): q = forms.CharField( label=_('Search'), widget=forms.TextInput( diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 0b0e2036e..7e1eaa80c 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -7,7 +7,7 @@ from extras.choices import * from extras.models import CustomField, Tag from utilities.forms import CSVModelForm from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField -from utilities.forms.mixins import BootstrapMixin, CheckLastUpdatedMixin +from utilities.forms.mixins import CheckLastUpdatedMixin from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin __all__ = ( @@ -18,7 +18,7 @@ __all__ = ( ) -class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm): +class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm): """ Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields. @@ -96,7 +96,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): return customfield.to_form_field(for_csv_import=True) -class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form): +class NetBoxModelBulkEditForm(CustomFieldsMixin, forms.Form): """ Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom fields and adding/removing tags. @@ -146,7 +146,7 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form): self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields) -class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, SavedFiltersMixin, forms.Form): +class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form): """ Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the corresponding FilterSet *must* provide a `q` filter. diff --git a/netbox/netbox/navigation/__init__.py b/netbox/netbox/navigation/__init__.py index 4c7190bbb..63d2af9c1 100644 --- a/netbox/netbox/navigation/__init__.py +++ b/netbox/netbox/navigation/__init__.py @@ -79,8 +79,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')): link=f'{app_label}:{model_name}_add', title='Add', icon_class='mdi mdi-plus-thick', - permissions=[f'{app_label}.add_{model_name}'], - color=ButtonColorChoices.GREEN + permissions=[f'{app_label}.add_{model_name}'] ) ) if 'import' in actions: @@ -89,8 +88,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')): link=f'{app_label}:{model_name}_import', title='Import', icon_class='mdi mdi-upload', - permissions=[f'{app_label}.add_{model_name}'], - color=ButtonColorChoices.CYAN + permissions=[f'{app_label}.add_{model_name}'] ) ) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index d4969386e..0262a5f98 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -377,24 +377,22 @@ ADMIN_MENU = Menu( items=( # Proxy model for auth.User MenuItem( - link=f'users:netboxuser_list', + link=f'users:user_list', link_text=_('Users'), permissions=[f'auth.view_user'], staff_only=True, buttons=( MenuItemButton( - link=f'users:netboxuser_add', + link=f'users:user_add', title='Add', icon_class='mdi mdi-plus-thick', - permissions=[f'auth.add_user'], - color=ButtonColorChoices.GREEN + permissions=[f'auth.add_user'] ), MenuItemButton( - link=f'users:netboxuser_import', + link=f'users:user_import', title='Import', icon_class='mdi mdi-upload', - permissions=[f'auth.add_user'], - color=ButtonColorChoices.CYAN + permissions=[f'auth.add_user'] ) ) ), @@ -409,15 +407,13 @@ ADMIN_MENU = Menu( link=f'users:netboxgroup_add', title='Add', icon_class='mdi mdi-plus-thick', - permissions=[f'auth.add_group'], - color=ButtonColorChoices.GREEN + permissions=[f'auth.add_group'] ), MenuItemButton( link=f'users:netboxgroup_import', title='Import', icon_class='mdi mdi-upload', - permissions=[f'auth.add_group'], - color=ButtonColorChoices.CYAN + permissions=[f'auth.add_group'] ) ) ), @@ -454,6 +450,21 @@ ADMIN_MENU = Menu( ), ), ), + MenuGroup( + label=_('System'), + items=( + MenuItem( + link='core:plugin_list', + link_text=_('Plugins'), + staff_only=True + ), + MenuItem( + link='core:background_queue_list', + link_text=_('Background Tasks'), + staff_only=True + ), + ), + ), ), ) diff --git a/netbox/netbox/plugins/urls.py b/netbox/netbox/plugins/urls.py index 2f237f56a..075bda811 100644 --- a/netbox/netbox/plugins/urls.py +++ b/netbox/netbox/plugins/urls.py @@ -15,9 +15,6 @@ plugin_api_patterns = [ path('', views.PluginsAPIRootView.as_view(), name='api-root'), path('installed-plugins/', views.InstalledPluginsAPIView.as_view(), name='plugins-list') ] -plugin_admin_patterns = [ - path('installed-plugins/', staff_member_required(views.InstalledPluginsAdminView.as_view()), name='plugins_list') -] # Register base/API URL patterns for each plugin for plugin_path in settings.PLUGINS: diff --git a/netbox/netbox/plugins/views.py b/netbox/netbox/plugins/views.py index 5971f78ef..777a4c69e 100644 --- a/netbox/netbox/plugins/views.py +++ b/netbox/netbox/plugins/views.py @@ -12,17 +12,6 @@ from rest_framework.reverse import reverse from rest_framework.views import APIView -class InstalledPluginsAdminView(View): - """ - Admin view for listing all installed plugins - """ - def get(self, request): - plugins = [apps.get_app_config(plugin) for plugin in settings.PLUGINS] - return render(request, 'extras/admin/plugins_list.html', { - 'plugins': plugins, - }) - - @extend_schema(exclude=True) class InstalledPluginsAPIView(APIView): """ diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index ae470e5c5..743233cbe 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ from netbox.plugins import PluginConfig # Environment setup # -VERSION = '3.7-beta1' +VERSION = '3.7.3-dev' # Hostname HOSTNAME = platform.node() @@ -115,6 +115,7 @@ DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', { 'users.delete_token': ({'user': '$user'},), }) DEVELOPER = getattr(configuration, 'DEVELOPER', False) +DJANGO_ADMIN_ENABLED = getattr(configuration, 'DJANGO_ADMIN_ENABLED', False) DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs')) EMAIL = getattr(configuration, 'EMAIL', {}) EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', ( @@ -123,7 +124,6 @@ EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', ( EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {}) FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440) -GIT_PATH = getattr(configuration, 'GIT_PATH', 'git') HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None) INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1')) JINJA2_FILTERS = getattr(configuration, 'JINJA2_FILTERS', {}) @@ -355,7 +355,6 @@ SERVER_EMAIL = EMAIL.get('FROM_EMAIL') # INSTALLED_APPS = [ - 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -367,6 +366,7 @@ INSTALLED_APPS = [ 'debug_toolbar', 'graphiql_debug_toolbar', 'django_filters', + 'django_htmx', 'django_tables2', 'django_prometheus', 'strawberry_django', @@ -392,6 +392,9 @@ INSTALLED_APPS = [ 'drf_spectacular_sidecar', ] +if DJANGO_ADMIN_ENABLED: + INSTALLED_APPS.insert(0, 'django.contrib.admin') + # Middleware MIDDLEWARE = [ "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware", @@ -405,6 +408,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', + 'django_htmx.middleware.HtmxMiddleware', 'netbox.middleware.RemoteUserMiddleware', 'netbox.middleware.CoreMiddleware', 'netbox.middleware.MaintenanceModeMiddleware', @@ -450,6 +454,8 @@ AUTHENTICATION_BACKENDS = [ 'netbox.authentication.ObjectPermissionBackend', ] +AUTH_USER_MODEL = 'users.User' + # Time zones USE_TZ = True @@ -590,6 +596,8 @@ for param in dir(configuration): SOCIAL_AUTH_JSONFIELD_ENABLED = True SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username' +SOCIAL_AUTH_USER_MODEL = AUTH_USER_MODEL + # # Django Prometheus # @@ -727,8 +735,10 @@ LANGUAGES = ( ('en', _('English')), ('es', _('Spanish')), ('fr', _('French')), + ('ja', _('Japanese')), ('pt', _('Portuguese')), ('ru', _('Russian')), + ('tr', _('Turkish')), ) LOCALE_PATHS = ( diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index d2cd0a0d4..442e5f260 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -161,8 +161,11 @@ class ToggleColumn(tables.CheckBoxColumn): visible = kwargs.pop('visible', False) if 'attrs' not in kwargs: kwargs['attrs'] = { + 'th': { + 'class': 'w-1', + }, 'td': { - 'class': 'min-width', + 'class': 'w-1', }, 'input': { 'class': 'form-check-input' @@ -322,7 +325,7 @@ class ChoiceFieldColumn(tables.Column): except AttributeError: bg_color = self.DEFAULT_BG_COLOR - return mark_safe(f'{value}') + return mark_safe(f'{value}') def value(self, value): return value diff --git a/netbox/netbox/tables/template_code.py b/netbox/netbox/tables/template_code.py index 60bfda0c9..aaa5429ff 100644 --- a/netbox/netbox/tables/template_code.py +++ b/netbox/netbox/tables/template_code.py @@ -1,6 +1,6 @@ SEARCH_RESULT_ATTRS = """ {% for name, value in record.display_attrs.items %} - 40 %} data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ value }}"{% endif %} > {{ name|bettertitle }}: diff --git a/netbox/netbox/tests/dummy_plugin/migrations/0001_initial.py b/netbox/netbox/tests/dummy_plugin/migrations/0001_initial.py index 4342d9576..b7241b51d 100644 --- a/netbox/netbox/tests/dummy_plugin/migrations/0001_initial.py +++ b/netbox/netbox/tests/dummy_plugin/migrations/0001_initial.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='DummyModel', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), ('name', models.CharField(max_length=20)), ('number', models.IntegerField(default=100)), ], diff --git a/netbox/netbox/tests/dummy_plugin/navigation.py b/netbox/netbox/tests/dummy_plugin/navigation.py index 4e7bb4be8..803cb67dc 100644 --- a/netbox/netbox/tests/dummy_plugin/navigation.py +++ b/netbox/netbox/tests/dummy_plugin/navigation.py @@ -4,23 +4,23 @@ from netbox.plugins.navigation import PluginMenu, PluginMenuButton, PluginMenuIt items = ( PluginMenuItem( - link='plugins:dummy_plugin:dummy_models', + link='plugins:dummy_plugin:dummy_model_list', link_text='Item 1', buttons=( PluginMenuButton( - link='admin:dummy_plugin_dummymodel_add', - title='Add a new dummy model', + link='plugins:dummy_plugin:dummy_model_add', + title='Button 1', icon_class='mdi mdi-plus-thick', ), PluginMenuButton( - link='admin:dummy_plugin_dummymodel_add', - title='Add a new dummy model', + link='plugins:dummy_plugin:dummy_model_add', + title='Button 2', icon_class='mdi mdi-plus-thick', ), ) ), PluginMenuItem( - link='plugins:dummy_plugin:dummy_models', + link='plugins:dummy_plugin:dummy_model_list', link_text='Item 2', ), ) diff --git a/netbox/netbox/tests/dummy_plugin/urls.py b/netbox/netbox/tests/dummy_plugin/urls.py index 053a7443e..9e383ffe2 100644 --- a/netbox/netbox/tests/dummy_plugin/urls.py +++ b/netbox/netbox/tests/dummy_plugin/urls.py @@ -4,5 +4,6 @@ from . import views urlpatterns = ( - path('models/', views.DummyModelsView.as_view(), name='dummy_models'), + path('models/', views.DummyModelsView.as_view(), name='dummy_model_list'), + path('models/add/', views.DummyModelAddView.as_view(), name='dummy_model_add'), ) diff --git a/netbox/netbox/tests/dummy_plugin/views.py b/netbox/netbox/tests/dummy_plugin/views.py index 03a83b585..f6cf6a5c5 100644 --- a/netbox/netbox/tests/dummy_plugin/views.py +++ b/netbox/netbox/tests/dummy_plugin/views.py @@ -1,3 +1,6 @@ +import random +import string + from django.http import HttpResponse from django.views.generic import View @@ -15,6 +18,20 @@ class DummyModelsView(View): return HttpResponse(f"Instances: {instance_count}") +class DummyModelAddView(View): + + def get(self, request): + return HttpResponse(f"Create an instance") + + def post(self, request): + instance = DummyModel( + name=''.join(random.choices(string.ascii_lowercase, k=8)), + number=random.randint(1, 100000) + ) + instance.save() + return HttpResponse(f"Instance created") + + @register_model_view(Site, 'extra', path='other-stuff') class ExtraCoreModelView(View): diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py index 40bf8b0ea..f7aeb7a53 100644 --- a/netbox/netbox/tests/test_plugins.py +++ b/netbox/netbox/tests/test_plugins.py @@ -41,7 +41,7 @@ class PluginTest(TestCase): def test_views(self): # Test URL resolution - url = reverse('plugins:dummy_plugin:dummy_models') + url = reverse('plugins:dummy_plugin:dummy_model_list') self.assertEqual(url, '/plugins/dummy-plugin/models/') # Test GET request diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 6776f6dde..cf1086f99 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -9,10 +9,9 @@ from account.views import LoginView, LogoutView from netbox.api.views import APIRootView, StatusView from netbox.graphql.schema import schema from netbox.graphql.views import GraphQLView -from netbox.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns +from netbox.plugins.urls import plugin_patterns, plugin_api_patterns from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx from strawberry.django.views import GraphQLView -from .admin import admin_site _patterns = [ @@ -71,28 +70,25 @@ _patterns = [ # Plugins path('plugins/', include((plugin_patterns, 'plugins'))), path('api/plugins/', include((plugin_api_patterns, 'plugins-api'))), - - # Admin - path('admin/background-tasks/', include('django_rq.urls')), - path('admin/plugins/', include(plugin_admin_patterns)), - path('admin/', admin_site.urls), ] +# Django admin UI +if settings.DJANGO_ADMIN_ENABLED: + from .admin import admin_site + _patterns.append(path('admin/', admin_site.urls)) +# django-debug-toolbar if settings.DEBUG: import debug_toolbar - _patterns += [ - path('__debug__/', include(debug_toolbar.urls)), - ] + _patterns.append(path('__debug__/', include(debug_toolbar.urls))) +# Prometheus metrics if settings.METRICS_ENABLED: - _patterns += [ - path('', include('django_prometheus.urls')), - ] + _patterns.append(path('', include('django_prometheus.urls'))) # Prepend BASE_PATH urlpatterns = [ - path('{}'.format(settings.BASE_PATH), include(_patterns)) + path(settings.BASE_PATH, include(_patterns)) ] handler404 = 'netbox.views.errors.handler_404' diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 615db6181..f5b605ccd 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -22,7 +22,6 @@ from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields from utilities.forms.bulk_import import BulkImportForm -from utilities.htmx import is_embedded, is_htmx from utilities.permissions import get_permission_for_model from utilities.utils import get_viewname from utilities.views import GetReturnURLMixin @@ -162,8 +161,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): table = self.get_table(self.queryset, request, has_bulk_actions) # If this is an HTMX request, return only the rendered table HTML - if is_htmx(request): - if is_embedded(request): + if request.htmx: + if request.htmx.target != 'object_list': table.embedded = True # Hide selection checkboxes if 'pk' in table.base_columns: diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 90b6e9495..10ea257bd 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -16,7 +16,6 @@ from extras.signals import clear_events from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, PermissionsViolation from utilities.forms import ConfirmationForm, restrict_form_fields -from utilities.htmx import is_htmx from utilities.permissions import get_permission_for_model from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fields from utilities.views import GetReturnURLMixin @@ -136,7 +135,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin): table = self.get_table(table_data, request, has_bulk_actions) # If this is an HTMX request, return only the rendered table HTML - if is_htmx(request): + if request.htmx: return render(request, 'htmx/table.html', { 'object': instance, 'table': table, @@ -224,7 +223,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): restrict_form_fields(form, request.user) # If this is an HTMX request, return only the rendered form HTML - if is_htmx(request): + if request.htmx: return render(request, 'htmx/form.html', { 'form': form, }) @@ -349,7 +348,7 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView): """ handle_protectederror(protected_objects, request, exc) - if is_htmx(request): + if request.htmx: return HttpResponse(headers={ 'HX-Redirect': obj.get_absolute_url(), }) @@ -378,7 +377,7 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView): return self._handle_protected_objects(obj, e.restricted_objects, request, e) # If this is an HTMX request, return only the rendered deletion form as modal content - if is_htmx(request): + if request.htmx: viewname = get_viewname(self.queryset.model, action='delete') form_url = reverse(viewname, kwargs={'pk': obj.pk}) return render(request, 'htmx/delete_form.html', { @@ -480,7 +479,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): instance = self.alter_object(self.queryset.model(), request) # If this is an HTMX request, return only the rendered form HTML - if is_htmx(request): + if request.htmx: return render(request, 'htmx/form.html', { 'form': form, }) diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index c7255916c..fc6c18218 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -2,19 +2,21 @@ import re from collections import namedtuple from django.conf import settings +from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.cache import cache from django.shortcuts import redirect, render +from django.utils.translation import gettext_lazy as _ from django.views.generic import View from django_tables2 import RequestConfig from packaging import version -from extras.dashboard.utils import get_dashboard +from extras.constants import DEFAULT_DASHBOARD +from extras.dashboard.utils import get_dashboard, get_default_dashboard from netbox.forms import SearchForm from netbox.search import LookupTypes from netbox.search.backends import search_backend from netbox.tables import SearchTable -from utilities.htmx import is_htmx from utilities.paginator import EnhancedPaginator, get_paginate_count __all__ = ( @@ -33,7 +35,13 @@ class HomeView(View): return redirect('login') # Construct the user's custom dashboard layout - dashboard = get_dashboard(request.user).get_layout() + try: + dashboard = get_dashboard(request.user).get_layout() + except Exception: + messages.error(request, _( + "There was an error loading the dashboard configuration. A default dashboard is in use." + )) + dashboard = get_default_dashboard(config=DEFAULT_DASHBOARD).get_layout() # Check whether a new release is available. (Only for staff/superusers.) new_release = None @@ -96,7 +104,7 @@ class SearchView(View): }).configure(table) # If this is an HTMX request, return only the rendered table HTML - if is_htmx(request): + if request.htmx: return render(request, 'htmx/table.html', { 'table': table, }) diff --git a/netbox/project-static/bundle.js b/netbox/project-static/bundle.js index 6f651cd05..85cf3c57b 100644 --- a/netbox/project-static/bundle.js +++ b/netbox/project-static/bundle.js @@ -73,12 +73,10 @@ async function bundleScripts() { async function bundleStyles() { try { const entryPoints = { - 'netbox-external': 'styles/_external.scss', - 'netbox-light': 'styles/_light.scss', - 'netbox-dark': 'styles/_dark.scss', - 'netbox-print': 'styles/_print.scss', - rack_elevation: 'styles/_rack_elevation.scss', - cable_trace: 'styles/_cable_trace.scss', + 'netbox-external': 'styles/external.scss', + 'netbox': 'styles/netbox.scss', + rack_elevation: 'styles/svg/rack_elevation.scss', + cable_trace: 'styles/svg/cable_trace.scss', graphiql: 'netbox-graphiql/graphiql.scss', }; const pluginOptions = { outputStyle: 'compressed' }; @@ -102,8 +100,7 @@ async function bundleStyles() { }); if (result.errors.length === 0) { for (const [targetName, sourceName] of Object.entries(entryPoints)) { - const source = sourceName.split('/')[1]; - console.log(`✅ Bundled source file '${source}' to '${targetName}.css'`); + console.log(`✅ Bundled source file '${sourceName}' to '${targetName}.css'`); } } } catch (err) { diff --git a/netbox/project-static/dist/Inter-Black-7VL4YR6G.woff b/netbox/project-static/dist/Inter-Black-7VL4YR6G.woff new file mode 100644 index 000000000..2f5cb41be Binary files /dev/null and b/netbox/project-static/dist/Inter-Black-7VL4YR6G.woff differ diff --git a/netbox/project-static/dist/Inter-Black-NU3KAVPI.woff2 b/netbox/project-static/dist/Inter-Black-NU3KAVPI.woff2 new file mode 100644 index 000000000..53c0aa239 Binary files /dev/null and b/netbox/project-static/dist/Inter-Black-NU3KAVPI.woff2 differ diff --git a/netbox/project-static/dist/Inter-BlackItalic-4BVAVUVI.woff2 b/netbox/project-static/dist/Inter-BlackItalic-4BVAVUVI.woff2 new file mode 100644 index 000000000..63ec88686 Binary files /dev/null and b/netbox/project-static/dist/Inter-BlackItalic-4BVAVUVI.woff2 differ diff --git a/netbox/project-static/dist/Inter-BlackItalic-HJLUEYVW.woff b/netbox/project-static/dist/Inter-BlackItalic-HJLUEYVW.woff new file mode 100644 index 000000000..a168e5820 Binary files /dev/null and b/netbox/project-static/dist/Inter-BlackItalic-HJLUEYVW.woff differ diff --git a/netbox/project-static/dist/Inter-Bold-6AVOS7AV.woff2 b/netbox/project-static/dist/Inter-Bold-6AVOS7AV.woff2 new file mode 100644 index 000000000..6989c9922 Binary files /dev/null and b/netbox/project-static/dist/Inter-Bold-6AVOS7AV.woff2 differ diff --git a/netbox/project-static/dist/Inter-Bold-RTIBL5DO.woff b/netbox/project-static/dist/Inter-Bold-RTIBL5DO.woff new file mode 100644 index 000000000..2ec7ac3d2 Binary files /dev/null and b/netbox/project-static/dist/Inter-Bold-RTIBL5DO.woff differ diff --git a/netbox/project-static/dist/Inter-BoldItalic-AID2XGS3.woff2 b/netbox/project-static/dist/Inter-BoldItalic-AID2XGS3.woff2 new file mode 100644 index 000000000..18b4c1ce5 Binary files /dev/null and b/netbox/project-static/dist/Inter-BoldItalic-AID2XGS3.woff2 differ diff --git a/netbox/project-static/dist/Inter-BoldItalic-UU7SOVRI.woff b/netbox/project-static/dist/Inter-BoldItalic-UU7SOVRI.woff new file mode 100644 index 000000000..aa35b7974 Binary files /dev/null and b/netbox/project-static/dist/Inter-BoldItalic-UU7SOVRI.woff differ diff --git a/netbox/project-static/dist/Inter-ExtraBold-B5RSZK6R.woff b/netbox/project-static/dist/Inter-ExtraBold-B5RSZK6R.woff new file mode 100644 index 000000000..d79d3d242 Binary files /dev/null and b/netbox/project-static/dist/Inter-ExtraBold-B5RSZK6R.woff differ diff --git a/netbox/project-static/dist/Inter-ExtraBold-VAPCTO5F.woff2 b/netbox/project-static/dist/Inter-ExtraBold-VAPCTO5F.woff2 new file mode 100644 index 000000000..3e74973f2 Binary files /dev/null and b/netbox/project-static/dist/Inter-ExtraBold-VAPCTO5F.woff2 differ diff --git a/netbox/project-static/dist/Inter-ExtraBoldItalic-R7DLWYIK.woff b/netbox/project-static/dist/Inter-ExtraBoldItalic-R7DLWYIK.woff new file mode 100644 index 000000000..06aad19b0 Binary files /dev/null and b/netbox/project-static/dist/Inter-ExtraBoldItalic-R7DLWYIK.woff differ diff --git a/netbox/project-static/dist/Inter-ExtraBoldItalic-W26CJYY7.woff2 b/netbox/project-static/dist/Inter-ExtraBoldItalic-W26CJYY7.woff2 new file mode 100644 index 000000000..77391ea7f Binary files /dev/null and b/netbox/project-static/dist/Inter-ExtraBoldItalic-W26CJYY7.woff2 differ diff --git a/netbox/project-static/dist/Inter-ExtraLight-A7NHN7KT.woff b/netbox/project-static/dist/Inter-ExtraLight-A7NHN7KT.woff new file mode 100644 index 000000000..fee5fca02 Binary files /dev/null and b/netbox/project-static/dist/Inter-ExtraLight-A7NHN7KT.woff differ diff --git a/netbox/project-static/dist/Inter-ExtraLight-DVW3SATI.woff2 b/netbox/project-static/dist/Inter-ExtraLight-DVW3SATI.woff2 new file mode 100644 index 000000000..607250b9a Binary files /dev/null and b/netbox/project-static/dist/Inter-ExtraLight-DVW3SATI.woff2 differ diff --git a/netbox/project-static/dist/Inter-ExtraLightItalic-KXZYPKOE.woff2 b/netbox/project-static/dist/Inter-ExtraLightItalic-KXZYPKOE.woff2 new file mode 100644 index 000000000..858d38a50 Binary files /dev/null and b/netbox/project-static/dist/Inter-ExtraLightItalic-KXZYPKOE.woff2 differ diff --git a/netbox/project-static/dist/Inter-ExtraLightItalic-SKGXKLLA.woff b/netbox/project-static/dist/Inter-ExtraLightItalic-SKGXKLLA.woff new file mode 100644 index 000000000..94e6582b5 Binary files /dev/null and b/netbox/project-static/dist/Inter-ExtraLightItalic-SKGXKLLA.woff differ diff --git a/netbox/project-static/dist/Inter-Italic-GHDVOOKA.woff b/netbox/project-static/dist/Inter-Italic-GHDVOOKA.woff new file mode 100644 index 000000000..4b765bd59 Binary files /dev/null and b/netbox/project-static/dist/Inter-Italic-GHDVOOKA.woff differ diff --git a/netbox/project-static/dist/Inter-Italic-RW2ZALTO.woff2 b/netbox/project-static/dist/Inter-Italic-RW2ZALTO.woff2 new file mode 100644 index 000000000..bd5f255a9 Binary files /dev/null and b/netbox/project-static/dist/Inter-Italic-RW2ZALTO.woff2 differ diff --git a/netbox/project-static/dist/Inter-Light-ARG5Y6MT.woff b/netbox/project-static/dist/Inter-Light-ARG5Y6MT.woff new file mode 100644 index 000000000..7590ff890 Binary files /dev/null and b/netbox/project-static/dist/Inter-Light-ARG5Y6MT.woff differ diff --git a/netbox/project-static/dist/Inter-Light-XBRUIIRZ.woff2 b/netbox/project-static/dist/Inter-Light-XBRUIIRZ.woff2 new file mode 100644 index 000000000..551410ac3 Binary files /dev/null and b/netbox/project-static/dist/Inter-Light-XBRUIIRZ.woff2 differ diff --git a/netbox/project-static/dist/Inter-LightItalic-4OWY6V5R.woff2 b/netbox/project-static/dist/Inter-LightItalic-4OWY6V5R.woff2 new file mode 100644 index 000000000..976d52564 Binary files /dev/null and b/netbox/project-static/dist/Inter-LightItalic-4OWY6V5R.woff2 differ diff --git a/netbox/project-static/dist/Inter-LightItalic-WFS25IQZ.woff b/netbox/project-static/dist/Inter-LightItalic-WFS25IQZ.woff new file mode 100644 index 000000000..8f2a4ca46 Binary files /dev/null and b/netbox/project-static/dist/Inter-LightItalic-WFS25IQZ.woff differ diff --git a/netbox/project-static/dist/Inter-Medium-JV63DMR7.woff2 b/netbox/project-static/dist/Inter-Medium-JV63DMR7.woff2 new file mode 100644 index 000000000..a916b47fc Binary files /dev/null and b/netbox/project-static/dist/Inter-Medium-JV63DMR7.woff2 differ diff --git a/netbox/project-static/dist/Inter-Medium-KH2CGZVE.woff b/netbox/project-static/dist/Inter-Medium-KH2CGZVE.woff new file mode 100644 index 000000000..7d55f34cc Binary files /dev/null and b/netbox/project-static/dist/Inter-Medium-KH2CGZVE.woff differ diff --git a/netbox/project-static/dist/Inter-MediumItalic-P7H6BBYU.woff2 b/netbox/project-static/dist/Inter-MediumItalic-P7H6BBYU.woff2 new file mode 100644 index 000000000..f623924ae Binary files /dev/null and b/netbox/project-static/dist/Inter-MediumItalic-P7H6BBYU.woff2 differ diff --git a/netbox/project-static/dist/Inter-MediumItalic-PO2E4FIG.woff b/netbox/project-static/dist/Inter-MediumItalic-PO2E4FIG.woff new file mode 100644 index 000000000..422ab0576 Binary files /dev/null and b/netbox/project-static/dist/Inter-MediumItalic-PO2E4FIG.woff differ diff --git a/netbox/project-static/dist/Inter-Regular-373XMOLM.woff2 b/netbox/project-static/dist/Inter-Regular-373XMOLM.woff2 new file mode 100644 index 000000000..554aed661 Binary files /dev/null and b/netbox/project-static/dist/Inter-Regular-373XMOLM.woff2 differ diff --git a/netbox/project-static/dist/Inter-Regular-DR3VC6YG.woff b/netbox/project-static/dist/Inter-Regular-DR3VC6YG.woff new file mode 100644 index 000000000..7ff51b7d8 Binary files /dev/null and b/netbox/project-static/dist/Inter-Regular-DR3VC6YG.woff differ diff --git a/netbox/project-static/dist/Inter-SemiBold-TJ6HPMMH.woff b/netbox/project-static/dist/Inter-SemiBold-TJ6HPMMH.woff new file mode 100644 index 000000000..76e507a51 Binary files /dev/null and b/netbox/project-static/dist/Inter-SemiBold-TJ6HPMMH.woff differ diff --git a/netbox/project-static/dist/Inter-SemiBold-UMRILHOG.woff2 b/netbox/project-static/dist/Inter-SemiBold-UMRILHOG.woff2 new file mode 100644 index 000000000..930799899 Binary files /dev/null and b/netbox/project-static/dist/Inter-SemiBold-UMRILHOG.woff2 differ diff --git a/netbox/project-static/dist/Inter-SemiBoldItalic-ABO2DCC7.woff2 b/netbox/project-static/dist/Inter-SemiBoldItalic-ABO2DCC7.woff2 new file mode 100644 index 000000000..f19f5505e Binary files /dev/null and b/netbox/project-static/dist/Inter-SemiBoldItalic-ABO2DCC7.woff2 differ diff --git a/netbox/project-static/dist/Inter-SemiBoldItalic-DM4JJSFU.woff b/netbox/project-static/dist/Inter-SemiBoldItalic-DM4JJSFU.woff new file mode 100644 index 000000000..382181212 Binary files /dev/null and b/netbox/project-static/dist/Inter-SemiBoldItalic-DM4JJSFU.woff differ diff --git a/netbox/project-static/dist/Inter-Thin-MEKOZMGA.woff b/netbox/project-static/dist/Inter-Thin-MEKOZMGA.woff new file mode 100644 index 000000000..6074d22b9 Binary files /dev/null and b/netbox/project-static/dist/Inter-Thin-MEKOZMGA.woff differ diff --git a/netbox/project-static/dist/Inter-Thin-TCINWRKS.woff2 b/netbox/project-static/dist/Inter-Thin-TCINWRKS.woff2 new file mode 100644 index 000000000..07d4fd6f2 Binary files /dev/null and b/netbox/project-static/dist/Inter-Thin-TCINWRKS.woff2 differ diff --git a/netbox/project-static/dist/Inter-ThinItalic-WRULPH3M.woff2 b/netbox/project-static/dist/Inter-ThinItalic-WRULPH3M.woff2 new file mode 100644 index 000000000..a824b2c98 Binary files /dev/null and b/netbox/project-static/dist/Inter-ThinItalic-WRULPH3M.woff2 differ diff --git a/netbox/project-static/dist/Inter-ThinItalic-ZFOL6JFL.woff b/netbox/project-static/dist/Inter-ThinItalic-ZFOL6JFL.woff new file mode 100644 index 000000000..e612ae45e Binary files /dev/null and b/netbox/project-static/dist/Inter-ThinItalic-ZFOL6JFL.woff differ diff --git a/netbox/project-static/dist/Inter-italic.var-7LS4VCXC.woff2 b/netbox/project-static/dist/Inter-italic.var-7LS4VCXC.woff2 new file mode 100644 index 000000000..03875311a Binary files /dev/null and b/netbox/project-static/dist/Inter-italic.var-7LS4VCXC.woff2 differ diff --git a/netbox/project-static/dist/Inter-roman.var-EDENVCUS.woff2 b/netbox/project-static/dist/Inter-roman.var-EDENVCUS.woff2 new file mode 100644 index 000000000..a6efdc486 Binary files /dev/null and b/netbox/project-static/dist/Inter-roman.var-EDENVCUS.woff2 differ diff --git a/netbox/project-static/dist/Inter.var-FTIQFLGO.woff2 b/netbox/project-static/dist/Inter.var-FTIQFLGO.woff2 new file mode 100644 index 000000000..b40083cbb Binary files /dev/null and b/netbox/project-static/dist/Inter.var-FTIQFLGO.woff2 differ diff --git a/netbox/project-static/dist/cable_trace.css b/netbox/project-static/dist/cable_trace.css index a5f5ff7e9..dc7a6a6fa 100644 Binary files a/netbox/project-static/dist/cable_trace.css and b/netbox/project-static/dist/cable_trace.css differ diff --git a/netbox/project-static/dist/netbox-external.css b/netbox/project-static/dist/netbox-external.css index edbb6aec5..9ec22015f 100644 Binary files a/netbox/project-static/dist/netbox-external.css and b/netbox/project-static/dist/netbox-external.css differ diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index 7a3cd7859..d766e5f6e 100644 Binary files a/netbox/project-static/dist/netbox-print.css and b/netbox/project-static/dist/netbox-print.css differ diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css new file mode 100644 index 000000000..194e79351 Binary files /dev/null and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 97c4ba79c..5146f855f 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index bbb2a3cc0..c28525be9 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/dist/rack_elevation.css b/netbox/project-static/dist/rack_elevation.css index 32b3b687f..5d6f06fbf 100644 Binary files a/netbox/project-static/dist/rack_elevation.css and b/netbox/project-static/dist/rack_elevation.css differ diff --git a/netbox/project-static/dist/roboto-mono-latin-100-JQJ4Z5FD.woff2 b/netbox/project-static/dist/roboto-mono-latin-100-JQJ4Z5FD.woff2 new file mode 100644 index 000000000..a92ac1ae7 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-100-JQJ4Z5FD.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-100-SMKVIC22.woff b/netbox/project-static/dist/roboto-mono-latin-100-SMKVIC22.woff new file mode 100644 index 000000000..4af7ecc07 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-100-SMKVIC22.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-100italic-DARDHGP2.woff b/netbox/project-static/dist/roboto-mono-latin-100italic-DARDHGP2.woff new file mode 100644 index 000000000..d96a43129 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-100italic-DARDHGP2.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-100italic-SF7I3UQS.woff2 b/netbox/project-static/dist/roboto-mono-latin-100italic-SF7I3UQS.woff2 new file mode 100644 index 000000000..986246098 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-100italic-SF7I3UQS.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-200-GAB3DPCB.woff2 b/netbox/project-static/dist/roboto-mono-latin-200-GAB3DPCB.woff2 new file mode 100644 index 000000000..e6411d1cc Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-200-GAB3DPCB.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-200-GNXGWAU7.woff b/netbox/project-static/dist/roboto-mono-latin-200-GNXGWAU7.woff new file mode 100644 index 000000000..d27217335 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-200-GNXGWAU7.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-200italic-MBEOZD5A.woff b/netbox/project-static/dist/roboto-mono-latin-200italic-MBEOZD5A.woff new file mode 100644 index 000000000..93460dfd4 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-200italic-MBEOZD5A.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-200italic-YC3AN6RQ.woff2 b/netbox/project-static/dist/roboto-mono-latin-200italic-YC3AN6RQ.woff2 new file mode 100644 index 000000000..baa44c85e Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-200italic-YC3AN6RQ.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-300-76QG53G7.woff2 b/netbox/project-static/dist/roboto-mono-latin-300-76QG53G7.woff2 new file mode 100644 index 000000000..1524f69fd Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-300-76QG53G7.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-300-KB4HCXJB.woff b/netbox/project-static/dist/roboto-mono-latin-300-KB4HCXJB.woff new file mode 100644 index 000000000..042abfc89 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-300-KB4HCXJB.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-300italic-ATWQWJG4.woff2 b/netbox/project-static/dist/roboto-mono-latin-300italic-ATWQWJG4.woff2 new file mode 100644 index 000000000..e33b97a12 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-300italic-ATWQWJG4.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-300italic-UCTTXECO.woff b/netbox/project-static/dist/roboto-mono-latin-300italic-UCTTXECO.woff new file mode 100644 index 000000000..98a54caaa Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-300italic-UCTTXECO.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-400-L25YYWY3.woff b/netbox/project-static/dist/roboto-mono-latin-400-L25YYWY3.woff new file mode 100644 index 000000000..f319fbfa4 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-400-L25YYWY3.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-400-OKRWGZOX.woff2 b/netbox/project-static/dist/roboto-mono-latin-400-OKRWGZOX.woff2 new file mode 100644 index 000000000..ed384d22f Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-400-OKRWGZOX.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-400italic-3G7XMJ7A.woff b/netbox/project-static/dist/roboto-mono-latin-400italic-3G7XMJ7A.woff new file mode 100644 index 000000000..22621fe22 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-400italic-3G7XMJ7A.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-400italic-UPQRVZWX.woff2 b/netbox/project-static/dist/roboto-mono-latin-400italic-UPQRVZWX.woff2 new file mode 100644 index 000000000..721ca2e93 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-400italic-UPQRVZWX.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-500-4ZB2P7GK.woff b/netbox/project-static/dist/roboto-mono-latin-500-4ZB2P7GK.woff new file mode 100644 index 000000000..e6ac1b966 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-500-4ZB2P7GK.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-500-67YXJMLO.woff2 b/netbox/project-static/dist/roboto-mono-latin-500-67YXJMLO.woff2 new file mode 100644 index 000000000..01b58ed2c Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-500-67YXJMLO.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-500italic-RXGCSZS4.woff b/netbox/project-static/dist/roboto-mono-latin-500italic-RXGCSZS4.woff new file mode 100644 index 000000000..a2b7d2c52 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-500italic-RXGCSZS4.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-500italic-U7WRIR22.woff2 b/netbox/project-static/dist/roboto-mono-latin-500italic-U7WRIR22.woff2 new file mode 100644 index 000000000..7554ceef0 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-500italic-U7WRIR22.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-600-USMEYMTS.woff b/netbox/project-static/dist/roboto-mono-latin-600-USMEYMTS.woff new file mode 100644 index 000000000..1833567f5 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-600-USMEYMTS.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-600-ZRZYGQNG.woff2 b/netbox/project-static/dist/roboto-mono-latin-600-ZRZYGQNG.woff2 new file mode 100644 index 000000000..c74ed699e Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-600-ZRZYGQNG.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-600italic-THUILB76.woff2 b/netbox/project-static/dist/roboto-mono-latin-600italic-THUILB76.woff2 new file mode 100644 index 000000000..aeb164f61 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-600italic-THUILB76.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-600italic-TLIW3M4P.woff b/netbox/project-static/dist/roboto-mono-latin-600italic-TLIW3M4P.woff new file mode 100644 index 000000000..0beb5da4d Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-600italic-TLIW3M4P.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-700-PEQVO3WV.woff b/netbox/project-static/dist/roboto-mono-latin-700-PEQVO3WV.woff new file mode 100644 index 000000000..ca550c033 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-700-PEQVO3WV.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-700-QNNSSZIK.woff2 b/netbox/project-static/dist/roboto-mono-latin-700-QNNSSZIK.woff2 new file mode 100644 index 000000000..c89a6eb38 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-700-QNNSSZIK.woff2 differ diff --git a/netbox/project-static/dist/roboto-mono-latin-700italic-KTWD2UYU.woff b/netbox/project-static/dist/roboto-mono-latin-700italic-KTWD2UYU.woff new file mode 100644 index 000000000..5f06e40e4 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-700italic-KTWD2UYU.woff differ diff --git a/netbox/project-static/dist/roboto-mono-latin-700italic-WWLRMKIN.woff2 b/netbox/project-static/dist/roboto-mono-latin-700italic-WWLRMKIN.woff2 new file mode 100644 index 000000000..837c3dc50 Binary files /dev/null and b/netbox/project-static/dist/roboto-mono-latin-700italic-WWLRMKIN.woff2 differ diff --git a/netbox/project-static/package-lock.json b/netbox/project-static/package-lock.json new file mode 100644 index 000000000..1c40ad5a3 --- /dev/null +++ b/netbox/project-static/package-lock.json @@ -0,0 +1,4108 @@ +{ + "name": "netbox", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "netbox", + "version": "0.1.0", + "license": "Apache-2.0", + "workspaces": [ + "netbox-graphiql" + ], + "dependencies": { + "@mdi/font": "^7.0.96", + "@tabler/core": "1.0.0-beta20", + "clipboard": "^2.0.11", + "color2k": "^2.0.0", + "dayjs": "^1.11.5", + "flatpickr": "4.6.13", + "gridstack": "^7.2.3", + "html-entities": "^2.3.3", + "htmx.org": "^1.8.0", + "just-debounce-it": "^3.1.1", + "query-string": "^7.1.1", + "sass": "^1.55.0", + "slim-select": "^1.27.1", + "typeface-inter": "^3.18.1", + "typeface-roboto-mono": "^1.1.13" + }, + "devDependencies": { + "@types/bootstrap": "5.2.10", + "@types/cookie": "^0.5.1", + "@typescript-eslint/eslint-plugin": "^5.39.0", + "@typescript-eslint/parser": "^5.39.0", + "esbuild": "^0.13.15", + "esbuild-sass-plugin": "^2.3.3", + "eslint": "^8.24.0", + "eslint-config-prettier": "^8.5.0", + "eslint-import-resolver-typescript": "^3.5.1", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^4.2.1", + "prettier": "^2.7.1", + "typescript": "~4.8.4" + } + }, + "netbox-graphiql": { + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "graphiql": "1.8.9", + "graphql": ">= v14.5.0 <= 15.5.0", + "react": "17.0.2", + "react-dom": "17.0.2", + "subscriptions-transport-ws": "0.9.18", + "whatwg-fetch": "3.6.2" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz", + "integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@graphiql/toolkit": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.4.5.tgz", + "integrity": "sha512-QXuuMSSK/0GfBS7tltrGZdyhIvm6oe9TK4VW9pfa8dALYttpzyJ64Q4Sx9I1Ng++yOMJWziM/ksa043zkNHsjQ==", + "license": "MIT", + "dependencies": { + "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0", + "meros": "^1.1.4" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "graphql-ws": ">= 4.5.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", + "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true, + "license": "Apache-2.0", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@mdi/font": { + "version": "7.0.96", + "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.0.96.tgz", + "integrity": "sha512-rzlxTfR64hqY8yiBzDjmANfcd8rv+T5C0Yedv/TWk2QyAQYdc66e0kaN1ipmnYU3RukHRTRcBARHzzm+tIhL7w==", + "license": "Apache-2.0" + }, + "node_modules/@n1ru4l/push-pull-async-iterable-iterator": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz", + "integrity": "sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz", + "integrity": "sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "is-glob": "^4.0.3", + "open": "^8.4.0", + "picocolors": "^1.0.0", + "tiny-glob": "^0.2.9", + "tslib": "^2.4.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@tabler/core": { + "version": "1.0.0-beta20", + "resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.0.0-beta20.tgz", + "integrity": "sha512-OzKpur+Ug7e+HMbNJrMcSuWZGUsJTvu7HYboBNRE8qyo1RKIWqvwL5YewKBJ+odW5pDOqBPzbsS4je3EBQQxHw==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.11.8", + "@tabler/icons": "^2.32.0", + "bootstrap": "5.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "@melloware/coloris": "^0.19.1", + "apexcharts": "^3.40.0", + "autosize": "^6.0.1", + "choices.js": "^10.2.0", + "countup.js": "^2.6.2", + "dropzone": "^6.0.0-beta.2", + "flatpickr": "^4.6.13", + "fslightbox": "^3.4.1", + "imask": "^6.6.1", + "jsvectormap": "^1.5.3", + "list.js": "^2.3.1", + "litepicker": "^2.0.12", + "nouislider": "^15.7.0", + "plyr": "^3.7.8", + "star-rating.js": "^4.3.0", + "tinymce": "^6.4.2", + "tom-select": "^2.2.2" + }, + "peerDependenciesMeta": { + "@melloware/coloris": { + "optional": true + }, + "apexcharts": { + "optional": true + }, + "autosize": { + "optional": true + }, + "choices.js": { + "optional": true + }, + "countup.js": { + "optional": true + }, + "dropzone": { + "optional": true + }, + "flatpickr": { + "optional": true + }, + "fslightbox": { + "optional": true + }, + "imask": { + "optional": true + }, + "jsvectormap": { + "optional": true + }, + "list.js": { + "optional": true + }, + "litepicker": { + "optional": true + }, + "nouislider": { + "optional": true + }, + "plyr": { + "optional": true + }, + "star-rating.js": { + "optional": true + }, + "tinymce": { + "optional": true + }, + "tom-select": { + "optional": true + } + } + }, + "node_modules/@tabler/icons": { + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-2.44.0.tgz", + "integrity": "sha512-WPPtihDcAwEm1QZM9MXQw6+r/R2/qx7KMU1eegsi9DsqBLAb0W2kbt6e/syvd6j9c+6XNpRVBW1ziGqSWQAWOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@types/bootstrap": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz", + "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.2" + } + }, + "node_modules/@types/bootstrap/node_modules/@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@types/cookie": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", + "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.39.0.tgz", + "integrity": "sha512-xVfKOkBm5iWMNGKQ2fwX5GVgBuHmZBO1tCRwXmY5oAIsPscfwm2UADDuNB8ZVYCtpQvJK4xpjrK7jEhcJ0zY9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "5.39.0", + "@typescript-eslint/type-utils": "5.39.0", + "@typescript-eslint/utils": "5.39.0", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.39.0.tgz", + "integrity": "sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.39.0", + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/typescript-estree": "5.39.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz", + "integrity": "sha512-/I13vAqmG3dyqMVSZPjsbuNQlYS082Y7OMkwhCfLXYsmlI0ca4nkL7wJ/4gjX70LD4P8Hnw1JywUVVAwepURBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/visitor-keys": "5.39.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.39.0.tgz", + "integrity": "sha512-KJHJkOothljQWzR3t/GunL0TPKY+fGJtnpl+pX+sJ0YiKTz3q2Zr87SGTmFqsCMFrLt5E0+o+S6eQY0FAXj9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.39.0", + "@typescript-eslint/utils": "5.39.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.39.0.tgz", + "integrity": "sha512-gQMZrnfEBFXK38hYqt8Lkwt8f4U6yq+2H5VDSgP/qiTzC8Nw8JO3OuSUOQ2qW37S/dlwdkHDntkZM6SQhKyPhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz", + "integrity": "sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/visitor-keys": "5.39.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.39.0.tgz", + "integrity": "sha512-+DnY5jkpOpgj+EBtYPyHRjXampJfC0yUZZzfzLuUWVZvCuKqSdJVC8UhdWipIw7VKNTfwfAPiOWzYkAwuIhiAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.39.0", + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/typescript-estree": "5.39.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz", + "integrity": "sha512-yyE3RPwOG+XJBLrhvsxAidUgybJVQ/hG8BhiJo0k8JSAYfk/CshVcxf0HwP4Jt7WZZ6vLmxdo1p6EyN3tzFTkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.39.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes/node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" + }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bootstrap": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz", + "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "license": "MIT", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/codemirror": { + "version": "5.65.14", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.14.tgz", + "integrity": "sha512-VSNugIBDGt0OU9gDjeVr6fNkoFQznrWEUdAApMlXQNbfE8gGO19776D6MwSqF/V/w/sDwonsQ0z7KmmI9guScg==", + "license": "MIT" + }, + "node_modules/codemirror-graphql": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-1.3.2.tgz", + "integrity": "sha512-glwFsEVlH5TvxjSKGymZ1sNy37f3Mes58CB4fXOd0zy9+JzDL08Wti1b5ycy4vFZYghMDK1/Or/zRSjMAGtC2w==", + "license": "MIT", + "dependencies": { + "graphql-language-service": "^5.0.6" + }, + "peerDependencies": { + "@codemirror/language": "^0.20.0", + "codemirror": "^5.65.3", + "graphql": "^15.5.0 || ^16.0.0" + } + }, + "node_modules/codemirror-graphql/node_modules/graphql-language-service": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.0.6.tgz", + "integrity": "sha512-FjE23aTy45Lr5metxCv3ZgSKEZOzN7ERR+OFC1isV5mHxI0Ob8XxayLTYjQKrs8b3kOpvgTYmSmu6AyXOzYslg==", + "license": "MIT", + "dependencies": { + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.15.1" + }, + "bin": { + "graphql": "dist/temp-bin.js" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0" + } + }, + "node_modules/codemirror-graphql/node_modules/vscode-languageserver-types": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", + "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color2k": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.0.tgz", + "integrity": "sha512-DWX9eXOC4fbJNiuvdH4QSHvvfLWyFo9TuFp7V9OzdsbPAdrWAuYc8qvFP2bIQ/LKh4LrAVnJ6vhiQYPvAHdtTg==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz", + "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true, + "license": "MIT" + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", + "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz", + "integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.6", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive/node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.13.15", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz", + "integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "optionalDependencies": { + "esbuild-android-arm64": "0.13.15", + "esbuild-darwin-64": "0.13.15", + "esbuild-darwin-arm64": "0.13.15", + "esbuild-freebsd-64": "0.13.15", + "esbuild-freebsd-arm64": "0.13.15", + "esbuild-linux-32": "0.13.15", + "esbuild-linux-64": "0.13.15", + "esbuild-linux-arm": "0.13.15", + "esbuild-linux-arm64": "0.13.15", + "esbuild-linux-mips64le": "0.13.15", + "esbuild-linux-ppc64le": "0.13.15", + "esbuild-netbsd-64": "0.13.15", + "esbuild-openbsd-64": "0.13.15", + "esbuild-sunos-64": "0.13.15", + "esbuild-windows-32": "0.13.15", + "esbuild-windows-64": "0.13.15", + "esbuild-windows-arm64": "0.13.15" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.13.15", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz", + "integrity": "sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-sass-plugin": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.3.3.tgz", + "integrity": "sha512-EegGnUIsP5Y7FbwcGBD524F+cJaIAQU2LSOX9QtjgpqEmwnmfEh5f/aPJ1df5GxD3NgHQJspeRCV7spDHE3N6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.14.13", + "resolve": "^1.22.1", + "sass": "^1.49.0" + } + }, + "node_modules/esbuild-sass-plugin/node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-sass-plugin/node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", + "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/eslintrc": "^1.3.2", + "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@humanwhocodes/module-importer": "^1.0.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.1.tgz", + "integrity": "sha512-U7LUjNJPYjNsHvAUAkt/RU3fcTSpbllA0//35B4eLYTX74frmOepbt7F7J3D1IGtj9k21buOpaqtDd4ZlS/BYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.10.0", + "get-tsconfig": "^4.2.0", + "globby": "^13.1.2", + "is-core-module": "^2.10.0", + "is-glob": "^4.0.3", + "synckit": "^0.8.3" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/globby": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz", + "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/glob-parent/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz", + "integrity": "sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic/node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.2.0.tgz", + "integrity": "sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-parent/node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "license": "MIT", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/graphiql": { + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-1.8.9.tgz", + "integrity": "sha512-X+olqol3VfOrFrsAfYzgNbvHoEE0lDUtg52+f3yLeNY4Ens+rAH4RSdH4wVtoRdh3CafmhVsfLQvn04hHkgftQ==", + "license": "MIT", + "dependencies": { + "@graphiql/toolkit": "^0.4.4", + "codemirror": "^5.65.3", + "codemirror-graphql": "^1.3.0", + "copy-to-clipboard": "^3.2.0", + "entities": "^2.0.0", + "escape-html": "^1.0.3", + "graphql-language-service": "^5.0.4", + "markdown-it": "^12.2.0", + "set-value": "^4.1.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/graphql": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz", + "integrity": "sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==", + "license": "MIT", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/graphql-language-service": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.1.7.tgz", + "integrity": "sha512-xkawYMJeoNYGhT+SpSH3c2qf6HpGHQ/duDmrseVHBpVCrXAiGnliXGSCC4jyMGgZQ05GytsZ12p0nUo7s6lSSw==", + "license": "MIT", + "dependencies": { + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.17.1" + }, + "bin": { + "graphql": "dist/temp-bin.js" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0" + } + }, + "node_modules/gridstack": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/gridstack/-/gridstack-7.2.3.tgz", + "integrity": "sha512-1s4Fx+Hr4nKl064q/ygrd41XiZaC2gG6R+yz5nbOibP9vODJ6mOtjIM5x8qKN12FknakaMpVBnCa1T6V7H15hQ==", + "funding": [ + { + "type": "paypal", + "url": "https://www.paypal.me/alaind831" + }, + { + "type": "venmo", + "url": "https://www.venmo.com/adumesny" + } + ], + "license": "MIT" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag/node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "license": "MIT" + }, + "node_modules/htmx.org": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.8.0.tgz", + "integrity": "sha512-xdR2PeSmhftFhUKn/5DYDFRVF8DagJR9d7y3AK+gQzoAQ+08r+0shaCTo1HdXKGKhRfX/uL3rqj4ZwCBNf8tLw==", + "license": "BSD 2-Clause" + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint/node_modules/has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol/node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", + "license": "MIT" + }, + "node_modules/js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/json5/node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/just-debounce-it": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-3.1.1.tgz", + "integrity": "sha512-oPsuRyWp99LJaQ4KXC3A42tQNqkRTcPy0A8BCkRZ5cPCgsx81upB2KUrmHZvDUNhnCDKe7MshfTuWFQB9iXwDg==", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meros": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz", + "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@types/node": ">=12" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true, + "license": "MIT" + }, + "node_modules/netbox-graphiql": { + "resolved": "netbox-graphiql", + "link": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.assign/node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/query-string": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", + "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test/node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sass": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", + "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", + "funding": [ + "https://github.com/sponsors/jonschlinkert", + "https://paypal.me/jonathanschlinkert", + "https://jonschlinkert.dev/sponsor" + ], + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + }, + "engines": { + "node": ">=11.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel/node_modules/object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slim-select": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/slim-select/-/slim-select-1.27.1.tgz", + "integrity": "sha512-LvJ02cKKk6/jSHIcQv7dZwkQSXHLCVQR3v3lo8RJUssUUcmKPkpBmTpQ8au8KSMkxwca9+yeg+dO0iHAaVr5Aw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend/node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart/node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/subscriptions-transport-ws": { + "version": "0.9.18", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz", + "integrity": "sha512-tztzcBTNoEbuErsVQpTN2xUNN/efAZXyCyL5m3x4t6SKrEiTL2N8SaKWBFWM4u56pL79ULif3zjyeq+oV+nOaA==", + "license": "MIT", + "dependencies": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0" + }, + "peerDependencies": { + "graphql": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/synckit": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.4.tgz", + "integrity": "sha512-Dn2ZkzMdSX827QbowGbU/4yjWuvNaCoScLLoMo/yKbu+P4GBR6cRGKZH27k6a9bRzdqcyd1DE96pQtQ6uNkmyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=", + "license": "MIT" + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typeface-inter": { + "version": "3.18.1", + "resolved": "https://registry.npmjs.org/typeface-inter/-/typeface-inter-3.18.1.tgz", + "integrity": "sha512-c+TBanYFCvmg3j5vPk+zxK4ocMZbPxMEmjnwG7rPQoV87xvQ6b07VbAOC0Va0XBbbZCGw6cWNeFuLeg1YQru3Q==" + }, + "node_modules/typeface-roboto-mono": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/typeface-roboto-mono/-/typeface-roboto-mono-1.1.13.tgz", + "integrity": "sha512-pnzDc70b7ywJHin/BUFL7HZX8DyOTBLT2qxlJ92eH1UJOFcENIBXa9IZrxsJX/gEKjbEDKhW5vz/TKRBNk/ufQ==" + }, + "node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==", + "license": "MIT" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", + "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/netbox/project-static/package.json b/netbox/project-static/package.json index 98e1a5c60..a46612d55 100644 --- a/netbox/project-static/package.json +++ b/netbox/project-static/package.json @@ -23,8 +23,7 @@ }, "dependencies": { "@mdi/font": "^7.0.96", - "@popperjs/core": "^2.11.6", - "bootstrap": "~5.0.2", + "@tabler/core": "1.0.0-beta20", "clipboard": "^2.0.11", "color2k": "^2.0.0", "dayjs": "^1.11.5", @@ -35,13 +34,13 @@ "just-debounce-it": "^3.1.1", "query-string": "^7.1.1", "sass": "^1.55.0", - "simplebar": "^5.3.9", - "slim-select": "^1.27.1" + "slim-select": "^1.27.1", + "typeface-inter": "^3.18.1", + "typeface-roboto-mono": "^1.1.13" }, "devDependencies": { - "@types/bootstrap": "^5.0.17", + "@types/bootstrap": "5.2.10", "@types/cookie": "^0.5.1", - "@types/masonry-layout": "^4.2.5", "@typescript-eslint/eslint-plugin": "^5.39.0", "@typescript-eslint/parser": "^5.39.0", "esbuild": "^0.13.15", diff --git a/netbox/project-static/src/colorMode.ts b/netbox/project-static/src/colorMode.ts index dfd05df4f..7883a9630 100644 --- a/netbox/project-static/src/colorMode.ts +++ b/netbox/project-static/src/colorMode.ts @@ -1,10 +1,6 @@ import { getElements, isTruthy } from './util'; const COLOR_MODE_KEY = 'netbox-color-mode'; -const TEXT_WHEN_DARK = 'Light Mode'; -const TEXT_WHEN_LIGHT = 'Dark Mode'; -const ICON_WHEN_DARK = 'mdi-lightbulb-on'; -const ICON_WHEN_LIGHT = 'mdi-lightbulb'; /** * Determine if a value is a supported color mode string value. @@ -24,23 +20,11 @@ function storeColorMode(mode: ColorMode): void { } function updateElements(targetMode: ColorMode): void { - document.documentElement.setAttribute(`data-${COLOR_MODE_KEY}`, targetMode); - - for (const text of getElements('span.color-mode-text')) { - if (targetMode === 'light') { - text.innerText = TEXT_WHEN_LIGHT; - } else if (targetMode === 'dark') { - text.innerText = TEXT_WHEN_DARK; - } - } - for (const icon of getElements('i.color-mode-icon', 'span.color-mode-icon')) { - if (targetMode === 'light') { - icon.classList.remove(ICON_WHEN_DARK); - icon.classList.add(ICON_WHEN_LIGHT); - } else if (targetMode === 'dark') { - icon.classList.remove(ICON_WHEN_LIGHT); - icon.classList.add(ICON_WHEN_DARK); - } + const body = document.querySelector('body'); + if (body && targetMode == 'dark') { + body.setAttribute('data-bs-theme', 'dark'); + } else if (body) { + body.setAttribute('data-bs-theme', 'light'); } for (const elevation of getElements('.rack_elevation')) { @@ -57,9 +41,8 @@ function updateElements(targetMode: ColorMode): void { * @param mode Target color mode. */ export function setColorMode(mode: ColorMode): void { - for (const func of [storeColorMode, updateElements]) { - func(mode); - } + storeColorMode(mode); + updateElements(mode); } /** diff --git a/netbox/project-static/src/index.ts b/netbox/project-static/src/index.ts index 266e69d6d..77c1e85ce 100644 --- a/netbox/project-static/src/index.ts +++ b/netbox/project-static/src/index.ts @@ -1,5 +1,4 @@ import '@popperjs/core'; import 'bootstrap'; import 'htmx.org'; -import 'simplebar'; import './netbox'; diff --git a/netbox/project-static/src/messages.ts b/netbox/project-static/src/messages.ts index e2ccabf5b..d17541e5f 100644 --- a/netbox/project-static/src/messages.ts +++ b/netbox/project-static/src/messages.ts @@ -5,7 +5,7 @@ import { Toast } from 'bootstrap'; */ export function initMessages(): void { const elements = document.querySelectorAll( - 'body > div#django-messages > div.django-message.toast', + 'body > div#django-messages > div.toast', ); for (const element of elements) { if (element !== null) { diff --git a/netbox/project-static/src/search.ts b/netbox/project-static/src/search.ts index e3bdc18dc..8275b126e 100644 --- a/netbox/project-static/src/search.ts +++ b/netbox/project-static/src/search.ts @@ -7,12 +7,12 @@ import { isTruthy } from './util'; */ function quickSearchEventHandler(event: Event): void { const quicksearch = event.currentTarget as HTMLInputElement; - const inputgroup = quicksearch.parentElement as HTMLDivElement; - if (isTruthy(inputgroup)) { + const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement; + if (isTruthy(clearbtn)) { if (quicksearch.value === "") { - inputgroup.classList.add("hide-last-child"); + clearbtn.classList.add("d-none"); } else { - inputgroup.classList.remove("hide-last-child"); + clearbtn.classList.remove("d-none"); } } } @@ -22,7 +22,7 @@ function quickSearchEventHandler(event: Event): void { */ export function initQuickSearch(): void { const quicksearch = document.getElementById("quicksearch") as HTMLInputElement; - const clearbtn = document.getElementById("quicksearch_clear") as HTMLButtonElement; + const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement; if (isTruthy(quicksearch)) { quicksearch.addEventListener("keyup", quickSearchEventHandler, { passive: true diff --git a/netbox/project-static/src/sidenav.ts b/netbox/project-static/src/sidenav.ts index d8207c9f7..cf2347980 100644 --- a/netbox/project-static/src/sidenav.ts +++ b/netbox/project-static/src/sidenav.ts @@ -204,23 +204,25 @@ class SideNav { * @param link Active nav link * @param action Expand or Collapse */ - private activateLink(link: HTMLAnchorElement, action: 'expand' | 'collapse'): void { - // Find the closest .collapse element, which should contain `link`. - const collapse = link.closest('.collapse') as Nullable; - if (isElement(collapse)) { - // Find the closest `.nav-link`, which should be adjacent to the `.collapse` element. - const groupLink = collapse.parentElement?.querySelector('.nav-link'); - if (isElement(groupLink)) { - groupLink.classList.add('active'); + private activateLink(link: HTMLDivElement, action: 'expand' | 'collapse'): void { + // Find the closest .dropdown-menu element, which should contain `link`. + const dropdownMenu = link.closest('.dropdown-menu') as Nullable; + if (isElement(dropdownMenu)) { + // Find the closest `.nav-link`, which should be adjacent to the `.dropdown-menu` element. + const groupItem = dropdownMenu.parentElement; + const groupLink = dropdownMenu.parentElement?.querySelector('.nav-link'); + if (isElement(groupLink) && isElement(groupItem)) { switch (action) { case 'expand': groupLink.setAttribute('aria-expanded', 'true'); - collapse.classList.add('show'); + groupItem.classList.add('active'); + dropdownMenu.classList.add('show'); link.classList.add('active'); break; case 'collapse': groupLink.setAttribute('aria-expanded', 'false'); - collapse.classList.remove('show'); + groupItem.classList.remove('active'); + dropdownMenu.classList.remove('show'); link.classList.remove('active'); break; } @@ -232,13 +234,16 @@ class SideNav { * Find any nav links with `href` attributes matching the current path, to determine which nav * link should be considered active. */ - private *getActiveLinks(): Generator { - for (const link of this.base.querySelectorAll( - '.navbar-nav .nav .nav-item a.nav-link', + private *getActiveLinks(): Generator { + for (const menuitem of this.base.querySelectorAll( + 'ul.navbar-nav .nav-item .dropdown-item', )) { - const href = new RegExp(link.href, 'gi'); - if (window.location.href.match(href)) { - yield link; + const link = menuitem.querySelector('a') + if (link) { + const href = new RegExp(link.href, 'gi'); + if (window.location.href.match(href)) { + yield menuitem; + } } } } @@ -309,7 +314,7 @@ class SideNav { } export function initSideNav(): void { - for (const sidenav of getElements('.sidenav')) { + for (const sidenav of getElements('.navbar')) { new SideNav(sidenav); } } diff --git a/netbox/project-static/styles/_cable_trace.scss b/netbox/project-static/styles/_cable_trace.scss deleted file mode 100644 index 66da051c5..000000000 --- a/netbox/project-static/styles/_cable_trace.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './theme-light.scss'; -@import './cable-trace.scss'; diff --git a/netbox/project-static/styles/_dark.scss b/netbox/project-static/styles/_dark.scss deleted file mode 100644 index c0c02e610..000000000 --- a/netbox/project-static/styles/_dark.scss +++ /dev/null @@ -1,10 +0,0 @@ -// Entry for netbox-dark.css stylesheet. - -html[data-netbox-color-mode='dark'] { - // Imports are scoped under the body when its data-netbox-color-mode attribute is set to 'dark'. - @import './theme-dark.scss'; - @import './bootstrap.scss'; - @import './select.scss'; - @import './flatpickr-dark.scss'; - @import './netbox.scss'; -} diff --git a/netbox/project-static/styles/_external.scss b/netbox/project-static/styles/_external.scss deleted file mode 100644 index a44238653..000000000 --- a/netbox/project-static/styles/_external.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Entry for all 3rd party library imports that do not rely on Bootstrap or NetBox styles. -@import '../node_modules/@mdi/font/css/materialdesignicons.min.css'; -@import '../node_modules/flatpickr/dist/flatpickr.css'; -@import '../node_modules/simplebar/dist/simplebar.css'; -@import 'gridstack/dist/gridstack.min.css'; diff --git a/netbox/project-static/styles/_light.scss b/netbox/project-static/styles/_light.scss deleted file mode 100644 index af0bbaab8..000000000 --- a/netbox/project-static/styles/_light.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Entry for netbox-light.css stylesheet. - -@import './theme-light.scss'; -@import './bootstrap.scss'; -@import './select.scss'; -@import './netbox.scss'; diff --git a/netbox/project-static/styles/_print.scss b/netbox/project-static/styles/_print.scss deleted file mode 100644 index 735f591d8..000000000 --- a/netbox/project-static/styles/_print.scss +++ /dev/null @@ -1,18 +0,0 @@ -// Entry for netbox-print.css. Force light-mode theming when printing. - -@media print { - // Force black and white background/foreground colors when printing. - :root { - --nbx-body-bg: #fff !important; - --nbx-body-color: #000 !important; - } - - html, - html[data-netbox-color-mode='dark'], - html[data-netbox-color-mode='light'] { - @import './theme-light'; - @import './bootstrap'; - @import './select'; - @import './netbox'; - } -} diff --git a/netbox/project-static/styles/_rack_elevation.scss b/netbox/project-static/styles/_rack_elevation.scss deleted file mode 100644 index a3f428db2..000000000 --- a/netbox/project-static/styles/_rack_elevation.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './theme-light.scss'; -@import './rack-elevation.scss'; diff --git a/netbox/project-static/styles/_variables.scss b/netbox/project-static/styles/_variables.scss new file mode 100644 index 000000000..6ac3c4896 --- /dev/null +++ b/netbox/project-static/styles/_variables.scss @@ -0,0 +1,23 @@ +// Global variables + +// Set base fonts +$font-family-base: 'Inter'; +// See https://github.com/tabler/tabler/issues/1812 +$font-family-monospace: 'Roboto Mono'; + +// Set the navigation sidebar width +$sidebar-width: 18rem; + +// Reduce the default button padding +$btn-padding-x: 0.5rem; +$btn-padding-y: 0.25rem; + +// Reduce the default table cell padding +$table-cell-padding-x: 0.5rem; +$table-cell-padding-y: 0.5rem; + +// Fix Tabler bug #1694 in 1.0.0-beta20 +$hover-bg: rgba(var(--tblr-secondary-rgb), 0.08); + +// Ensure active nav-pill has a background color in dark mode +$nav-pills-link-active-bg: rgba(var(--tblr-secondary-rgb), 0.15); diff --git a/netbox/project-static/styles/bootstrap.scss b/netbox/project-static/styles/bootstrap.scss deleted file mode 100644 index c55232acc..000000000 --- a/netbox/project-static/styles/bootstrap.scss +++ /dev/null @@ -1,35 +0,0 @@ -// Import the rest of bootstrap. -@import '../node_modules/bootstrap/scss/utilities'; -@import './extensions'; -@import '../node_modules/bootstrap/scss/mixins'; -@import '../node_modules/bootstrap/scss/root'; -@import '../node_modules/bootstrap/scss/reboot'; -@import '../node_modules/bootstrap/scss/type'; -@import '../node_modules/bootstrap/scss/images'; -@import '../node_modules/bootstrap/scss/containers'; -@import '../node_modules/bootstrap/scss/grid'; -@import '../node_modules/bootstrap/scss/tables'; -@import '../node_modules/bootstrap/scss/forms'; -@import '../node_modules/bootstrap/scss/buttons'; -@import '../node_modules/bootstrap/scss/transitions'; -@import '../node_modules/bootstrap/scss/dropdown'; -@import '../node_modules/bootstrap/scss/button-group'; -@import '../node_modules/bootstrap/scss/nav'; -@import '../node_modules/bootstrap/scss/navbar'; -@import '../node_modules/bootstrap/scss/card'; -@import '../node_modules/bootstrap/scss/accordion'; -@import '../node_modules/bootstrap/scss/breadcrumb'; -@import '../node_modules/bootstrap/scss/pagination'; -@import '../node_modules/bootstrap/scss/badge'; -@import '../node_modules/bootstrap/scss/alert'; -@import '../node_modules/bootstrap/scss/progress'; -@import '../node_modules/bootstrap/scss/list-group'; -@import '../node_modules/bootstrap/scss/close'; -@import '../node_modules/bootstrap/scss/toasts'; -@import '../node_modules/bootstrap/scss/modal'; -@import '../node_modules/bootstrap/scss/tooltip'; -@import '../node_modules/bootstrap/scss/popover'; -@import '../node_modules/bootstrap/scss/carousel'; -@import '../node_modules/bootstrap/scss/spinners'; -@import '../node_modules/bootstrap/scss/helpers'; -@import '../node_modules/bootstrap/scss/utilities/api'; diff --git a/netbox/project-static/styles/custom/_code.scss b/netbox/project-static/styles/custom/_code.scss new file mode 100644 index 000000000..4b068b94d --- /dev/null +++ b/netbox/project-static/styles/custom/_code.scss @@ -0,0 +1,40 @@ +// Serialized data from change records +pre.change-data { + padding-right: 0; + padding-left: 0; + + // Display each line individually for highlighting + > span { + display: block; + padding-right: $spacer; + padding-left: $spacer; + + &.added { + background-color: $green; + } + + &.removed { + background-color: $red; + } + } +} + +// Change data diff w/added & removed data +pre.change-diff { + border-color: transparent; + + &.change-added { + background-color: $green; + } + + &.change-removed { + background-color: $red; + } +} + +//
 elements displayed with a border
+pre.block {
+  padding: $spacer;
+  border: 1px solid $border-color;
+  border-radius: $border-radius;
+}
diff --git a/netbox/project-static/styles/custom/_markdown.scss b/netbox/project-static/styles/custom/_markdown.scss
new file mode 100644
index 000000000..08de23581
--- /dev/null
+++ b/netbox/project-static/styles/custom/_markdown.scss
@@ -0,0 +1,38 @@
+// Rendered Markdown
+.rendered-markdown {
+
+  table {
+    width: 100%;
+
+    // Apply a border
+    th {
+      border-bottom: 2px solid #dddddd;
+      padding: 8px;
+    }
+    td {
+      border-top: 1px solid #dddddd;
+      padding: 8px;
+    }
+
+    // Map "align" attr of column headings
+    th[align="left"] {
+      text-align: left;
+    }
+    th[align="center"] {
+      text-align: center;
+    }
+    th[align="right"] {
+      text-align: right;
+    }
+  }
+
+}
+
+// Markdown preview
+.markdown-widget {
+  .preview {
+    border: 1px solid $border-color;
+    border-radius: $border-radius;
+    min-height: 200px;
+  }
+}
diff --git a/netbox/project-static/styles/custom/_misc.scss b/netbox/project-static/styles/custom/_misc.scss
new file mode 100644
index 000000000..ebf66d547
--- /dev/null
+++ b/netbox/project-static/styles/custom/_misc.scss
@@ -0,0 +1,30 @@
+@use 'sass:map';
+
+// Color labels
+span.color-label {
+  display: block;
+  width: 5rem;
+  height: 1rem;
+  padding: $badge-padding-y $badge-padding-x;
+  border: 1px solid #303030;
+  border-radius: $border-radius;
+}
+
+// Object hierarchy depth indicators
+.record-depth {
+  display: inline;
+  user-select: none;
+  opacity: 33%;
+
+  // Add spacing to the last or only dot.
+  span:only-of-type,
+  span:last-of-type {
+    margin-right: map.get($spacers, 1);
+  }
+}
+
+// Hides the last child of an element
+.hide-last-child :last-child {
+  visibility: hidden;
+  opacity: 0;
+}
diff --git a/netbox/project-static/styles/extensions.scss b/netbox/project-static/styles/extensions.scss
deleted file mode 100644
index 79d19d17e..000000000
--- a/netbox/project-static/styles/extensions.scss
+++ /dev/null
@@ -1,45 +0,0 @@
-// Bootstrap utility extensions.
-// See https://getbootstrap.com/docs/5.0/utilities/api/
-
-// Add responsive max-width classes for more granular controls over max-width.
-$max-width-extension: (
-  'max-width': (
-    property: max-width,
-    class: mw,
-    responsive: true,
-    values: (
-      20: 20%,
-      25: 25%,
-      33: 33%,
-      50: 50%,
-      66: 66%,
-      75: 75%,
-      80: 80%,
-      90: 90%,
-      100: 100%,
-    ),
-  ),
-);
-
-// Add more opacity classes.
-$opacity-extension: (
-  'opacity': (
-    property: opacity,
-    class: opacity,
-    values: (
-      0: 0,
-      10: 0.1,
-      20: 0.2,
-      25: 0.25,
-      33: 0.33,
-      50: 0.5,
-      66: 0.66,
-      75: 0.75,
-      80: 0.8,
-      90: 0.9,
-      100: 1,
-    ),
-  ),
-);
-
-$utilities: map-merge($utilities, $max-width-extension, $opacity-extension);
diff --git a/netbox/project-static/styles/external.scss b/netbox/project-static/styles/external.scss
new file mode 100644
index 000000000..b3c83aa87
--- /dev/null
+++ b/netbox/project-static/styles/external.scss
@@ -0,0 +1,8 @@
+// Entry for all 3rd party library imports that do not rely on Tabler or NetBox styles.
+@import '../node_modules/@mdi/font/css/materialdesignicons.min.css';
+@import '../node_modules/flatpickr/dist/flatpickr.min.css';
+@import 'gridstack/dist/gridstack.min.css';
+
+// Fonts
+@import url("../node_modules/typeface-inter/inter.css");
+@import url("../node_modules/typeface-roboto-mono/index.css");
diff --git a/netbox/project-static/styles/flatpickr-dark.scss b/netbox/project-static/styles/flatpickr-dark.scss
deleted file mode 100644
index 94ebd775a..000000000
--- a/netbox/project-static/styles/flatpickr-dark.scss
+++ /dev/null
@@ -1,122 +0,0 @@
-// Dark Mode Styles for Flatpickr.
-
-$flatpickr-bg: $gray-800;
-$flatpickr-color: $body-color;
-$flatpickr-border-color: $gray-700;
-$flatpickr-title-color: $gray-300;
-$flatpickr-disabled-color: $gray-500;
-
-.flatpickr-calendar {
-  color: $flatpickr-color;
-  background: $flatpickr-bg;
-  border-radius: $border-radius;
-  box-shadow: 1px 0 0 $flatpickr-border-color, -1px 0 0 $flatpickr-border-color,
-    0 1px 0 $flatpickr-border-color, 0 -1px 0 $flatpickr-border-color, 0 3px 13px rgb(0 0 0 / 8%);
-
-  &.arrowTop:before,
-  &.arrowTop:after {
-    border-bottom-color: $flatpickr-bg;
-  }
-
-  span.flatpickr-weekday {
-    color: $flatpickr-title-color;
-  }
-
-  .numInputWrapper {
-    span.arrowUp:after {
-      border-bottom-color: $input-color;
-    }
-    span.arrowDown:after {
-      border-top-color: $input-color;
-    }
-  }
-
-  .flatpickr-months {
-    .flatpickr-month {
-      color: $flatpickr-color;
-      fill: $flatpickr-color;
-    }
-
-    .flatpickr-next-month,
-    .flatpickr-prev-month {
-      color: $flatpickr-color;
-      fill: $flatpickr-color;
-      &:hover svg {
-        fill: $danger;
-      }
-    }
-
-    .flatpickr-current-month select {
-      background: $flatpickr-bg;
-    }
-  }
-
-  .flatpickr-day {
-    color: $flatpickr-color;
-
-    &.selected,
-    &.startRange,
-    &.endRange,
-    &.selected.inRange,
-    &.startRange.inRange,
-    &.endRange.inRange,
-    &.selected:focus,
-    &.startRange:focus,
-    &.endRange:focus,
-    &.selected:hover,
-    &.startRange:hover,
-    &.endRange:hover,
-    &.selected.prevMonthDay,
-    &.startRange.prevMonthDay,
-    &.endRange.prevMonthDay,
-    &.selected.nextMonthDay,
-    &.startRange.nextMonthDay,
-    &.endRange.nextMonthDay {
-      color: color-contrast($blue-300);
-      background: $blue-300;
-      border-color: $blue-300;
-    }
-
-    &:hover {
-      color: color-contrast($secondary);
-      background: $secondary;
-      border-color: $secondary;
-    }
-
-    &.flatpickr-disabled,
-    &.flatpickr-disabled:hover,
-    &.prevMonthDay,
-    &.nextMonthDay,
-    &.notAllowed,
-    &.notAllowed.prevMonthDay,
-    &.notAllowed.nextMonthDay {
-      color: $flatpickr-disabled-color;
-
-      &:hover {
-        color: color-contrast($secondary);
-        background: $secondary;
-        border-color: $secondary;
-      }
-    }
-  }
-
-  .flatpickr-time {
-    input {
-      color: $input-color;
-      background: $flatpickr-bg;
-
-      &:hover,
-      &:active {
-        background: $flatpickr-bg;
-      }
-    }
-
-    .flatpickr-time-separator {
-      color: $flatpickr-disabled-color;
-    }
-  }
-
-  &.showTimeInput.hasTime .flatpickr-time {
-    border-top: 1px solid $flatpickr-border-color;
-  }
-}
diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss
index a38633b5c..17c6d88c9 100644
--- a/netbox/project-static/styles/netbox.scss
+++ b/netbox/project-static/styles/netbox.scss
@@ -1,1078 +1,23 @@
-// NetBox-specific Styles and Overrides.
-
-@use 'sass:map';
-@use 'sass:math';
-@import './sidenav';
-@import './overrides';
-@import './utilities';
-@import './variables';
-
-@each $color, $value in $theme-colors {
-  // Override CSS values on each theme color.
-
-  // Use Bootstrap's method of coloring alert links to appropriately color close buttons within
-  // another colored element.
-  // See: https://github.com/twbs/bootstrap/blob/2bdbb42dcf6bfb99b5e9e5444d9e64589eb8c08f/scss/_alert.scss#L50-L52
-  // See: https://github.com/twbs/bootstrap/blob/2bdbb42dcf6bfb99b5e9e5444d9e64589eb8c08f/scss/_close.scss#L12
-  $shifted-bg: shift-color($value, $alert-bg-scale);
-  $shifted-color: shift-color($value, $alert-color-scale);
-
-  @if (contrast-ratio($shifted-bg, $shifted-color) < $min-contrast-ratio) {
-    $shifted-color: mix($value, color-contrast($shifted-bg), abs($alert-color-scale));
-  }
-
-  $btn-close-bg: url("data:image/svg+xml,");
-  .bg-#{$color} button.btn-close {
-    background: transparent escape-svg($btn-close-bg) center / $btn-close-width auto no-repeat;
-  }
-
-  .btn.btn-ghost-#{$color} {
-    color: $value;
-    &:hover {
-      background-color: rgba($value, 0.12);
-    }
-  }
-
-  // Use Bootstrap's method of coloring the .alert-link class automatically.
-  // See: https://github.com/twbs/bootstrap/blob/2bdbb42dcf6bfb99b5e9e5444d9e64589eb8c08f/scss/_alert.scss#L50-L52
-
-  .alert.alert-#{$color},
-  .table-#{$color} {
-    // Exclude buttons.
-    a:not(.btn) {
-      font-weight: $font-weight-bold;
-      color: $shifted-color;
-    }
-    // Apply a border to buttons contained within colored elements, if they're not already a
-    // bordered button class.
-    .btn:not([class*='btn-outline']) {
-      border-color: $gray-700;
-    }
-  }
-
-  // Toasts required a slightly different approach because the background color isn't "shifted",
-  // it's the direct theme color.
-  .toast.bg-#{$color} {
-    $shifted-color: shift-color($value, $alert-color-scale);
-
-    @if (contrast-ratio($value, $shifted-color) < $min-contrast-ratio) {
-      $shifted-color: mix($value, color-contrast($value), abs($alert-color-scale));
-    }
-    a:not(.btn) {
-      font-weight: $font-weight-bold;
-      color: $shifted-color;
-    }
-  }
-
-  // Use proper contrasting color foreground color for special components.
-  .badge,
-  .toast,
-  .toast-header,
-  .progress-bar {
-    &.bg-#{$color} {
-      color: color-contrast($value);
-    }
-  }
-}
-
-// Ensure progress bars (utilization graph) in tables aren't too narrow to display the percentage.
-table td > .progress {
-  min-width: 6rem;
-}
-
-// Override Bootstrap form-control font-size when contained by .small element.
-.small .form-control {
-  font-size: $font-size-sm;
-}
-
-// Automatically space out adjacent columns, but not within card bodies.
-:not(.card-body) > .col:not(:last-child):not(:only-child) {
-  margin-bottom: $spacer;
-}
-
-.nav-mobile {
-  display: none;
-  flex-direction: column;
-  align-items: center;
-  justify-content: space-between;
-  width: 100%;
-
-  @include media-breakpoint-down(lg) {
-    display: flex;
-  }
-
-  .nav-mobile-top {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    width: 100%;
-  }
-}
-
-.card > .table.table-flush {
-  margin-bottom: 0;
-  overflow: hidden;
-  border-bottom-right-radius: $card-border-radius;
-  border-bottom-left-radius: $card-border-radius;
-
-  thead th[scope='col'] {
-    padding-top: map.get($spacers, 3);
-    padding-bottom: map.get($spacers, 3);
-    text-transform: uppercase;
-    vertical-align: middle;
-    background-color: $table-flush-header-bg;
-    border-top: 1px solid $card-border-color;
-    border-bottom-color: $card-border-color;
-  }
-
-  th,
-  td {
-    padding-right: map.get($spacers, 4) !important;
-    padding-left: map.get($spacers, 4) !important;
-    border-right: 0;
-    border-left: 0;
-  }
-  tr[class] {
-    border-color: $card-border-color !important;
-    &:last-of-type {
-      border-bottom-color: transparent !important;
-      border-bottom-right-radius: $card-border-radius;
-      border-bottom-left-radius: $card-border-radius;
-    }
-  }
-}
-
-// Primarily used for the new release notification, but could be used for other alerts as needed.
-// Wrap any alerts in .header-alert-container to ensure the layout is consistent.
-.header-alert-container {
-  // Center-align the alert(s).
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  // Apply the same spacing that's applied to the #content div's first child (.px-3).
-  padding: 0 $spacer;
-
-  // By default, alerts inside .header-alert-container should take up the full width.
-  .alert {
-    width: 100%;
-
-    // Adjust the max-width for larger screens so there's not a big ugly blue blob taking up the
-    // entire screen.
-    @include media-breakpoint-up(md) {
-      max-width: 75%;
-    }
-    @include media-breakpoint-up(lg) {
-      max-width: 50%;
-    }
-  }
-}
-
-.alert {
-  code {
-    color: $gray-600;
-  }
-}
-
-span.profile-button .dropdown-menu {
-  right: 0;
-  left: auto;
-  display: block !important;
-  margin-top: 0.5rem;
-  box-shadow: $box-shadow;
-  transition: opacity 0.2s ease-in-out;
-
-  &:not(.show) {
-    pointer-events: none;
-    opacity: 0;
-  }
-  &.show {
-    pointer-events: auto;
-    opacity: 1;
-  }
-}
-
-div#advanced-search-content div.card div.card-body div.col:not(:last-child) {
-  margin-right: 1rem;
-}
-
-table {
-  td {
-    a {
-      text-decoration: none;
-      &:hover {
-        text-decoration: underline;
-      }
-    }
-    .dropdown {
-      // Presence of 'overflow: scroll' on a table causes dropdowns to be improperly hidden when
-      // opened. See: https://github.com/twbs/bootstrap/issues/24251
-      position: static;
-    }
-  }
-  th {
-    a,
-    a:hover {
-      color: $body-color;
-      text-decoration: none;
-    }
-  }
-
-  td,
-  th {
-    font-size: $font-size-sm;
-    line-height: $line-height-sm;
-    vertical-align: middle;
-    &.min-width {
-      width: 1%;
-    }
-    .form-check-input {
-      // Ensure checkboxes aren't too small inside object tables.
-      margin-top: 0.125em;
-      font-size: $font-size-base;
-    }
-
-    .btn-sm {
-      line-height: $line-height-xs;
-    }
-
-    p {
-      // Remove spacing from paragraph elements within tables.
-      margin-bottom: 0.5em;
-    }
-
-    p:last-child {
-      margin-bottom: 0;
-    }
-  }
-
-  th.asc > a::after {
-    content: '\f0140';
-    font-family: 'Material Design Icons';
-  }
-
-  th.desc > a::after {
-    content: '\f0143';
-    font-family: 'Material Design Icons';
-  }
-
-  &.table > :not(caption) > * > * {
-    padding-right: $table-cell-padding-x-sm !important;
-    padding-left: $table-cell-padding-x-sm !important;
-  }
-
-  &.object-list {
-    th {
-      font-size: $font-size-xs;
-      line-height: $line-height-xs;
-      vertical-align: bottom;
-    }
-  }
-
-  &.attr-table {
-    th {
-      font-weight: normal;
-      width: 25%;
-    }
-  }
-}
-
-div.title-container {
-  display: flex;
-  // On small screens, `flex-direction: column;` ensures the control buttons don't appear on the
-  // same line as the title, but rather break to the next line.
-  flex-direction: column;
-  flex-wrap: wrap;
-  justify-content: space-between;
-
-  @include media-breakpoint-up(lg) {
-    // On large screens, `flex-direction: row;` allows the control buttons to appear vertically
-    // aligned with the title.
-    flex-direction: row;
-  }
-
-  #content-title {
-    display: flex;
-    flex: 1 0;
-    flex-direction: column;
-    padding-bottom: map.get($spacers, 2);
-  }
-}
-
-// Object list control buttons (Add/Clone/Import/Export)
-.controls {
-  margin-bottom: map.get($spacers, 2);
-
-  @media print {
-    // Never print controls. Use this over the .noprint utility so plugin authors don't need to
-    // remember to add the class.
-    display: none !important;
-  }
-
-  // Each group of buttons.
-  .control-group {
-    display: flex;
-    flex-wrap: wrap;
-    // Left-align controls on mobile.
-    justify-content: flex-start;
-
-    // Right-align controls on larger screens.
-    @include media-breakpoint-up(lg) {
-      justify-content: flex-end;
-    }
-
-    > * {
-      // Pad each control button.
-      margin: map.get($spacers, 1);
-
-      &:first-child {
-        // Don't pad the left side of the first control button.
-        margin-left: 0;
-      }
-
-      &:last-child {
-        // Don't pad the right side of the last control button.
-        margin-right: 0;
-      }
-    }
-  }
-}
-
-.object-subtitle {
-  display: block;
-  font-size: $font-size-sm;
-  color: $text-muted;
-
-  @include media-breakpoint-up(md) {
-    display: inline-block;
-  }
-
-  > span {
-    display: block;
-
-    // Hide the separator on small screens.
-    &.separator {
-      display: none;
-    }
-
-    &,
-    &.separator {
-      @include media-breakpoint-up(md) {
-        display: inline-block;
-      }
-    }
-  }
-}
-
-// Global Search
-nav.search {
-  // Don't overtake dropdowns
-  z-index: 999;
-  justify-content: center;
-  background-color: $navbar-light-color;
-
-  .search-container {
-    display: flex;
-    width: 100%;
-
-    @include media-breakpoint-down(lg) {
-      display: none;
-    }
-  }
-
-  // Search Input & Selected Object Value & Object Selector
-  .input-group {
-    // Selected Object
-    .search-obj-selected {
-      border-color: $input-border-color;
-    }
-
-    // Object Selector Dropdown Button
-    .dropdown-toggle {
-      // Generate the same styles as a regular Bootstrap button.
-      @include button-variant($input-group-addon-bg, $input-border-color);
-      margin-left: 0;
-      font-weight: $input-group-addon-font-weight;
-      line-height: $input-line-height;
-      color: $input-group-addon-color;
-      background-color: $input-group-addon-bg;
-      border: $input-border-width solid $input-border-color;
-      @include border-radius($input-border-radius);
-      border-left: 1px solid var(--nbx-search-filter-border-left-color);
-
-      &:focus {
-        box-shadow: unset !important;
-      }
-      // Don't show the dropdown icon — the filter icon is basically the same thing.
-      &:after {
-        display: none;
-      }
-    }
-
-    // Object Selector Dropdown Menu
-    .search-obj-selector {
-      // Limit the height and enable scrolling on mobile devices.
-      max-height: 70vh;
-      overflow-y: auto;
-
-      .dropdown-item,
-      .dropdown-header {
-        font-size: $font-size-sm;
-      }
-
-      .dropdown-header {
-        text-transform: uppercase;
-      }
-    }
-  }
-}
-
-// Styles for the quicksearch and its clear button;
-// Overrides input-group styles and adds transition effects
-.quicksearch {
-  input[type='search'] {
-    border-radius: $border-radius !important;
-  }
-
-  button {
-    margin-left: -32px !important;
-    z-index: 100 !important;
-    outline: none !important;
-    border-radius: $border-radius !important;
-    transition: visibility 0s, opacity 0.2s linear;
-  }
-
-  button :hover {
-    opacity: 50%;
-    transition: visibility 0s, opacity 0.1s linear;
-  }
-}
-
-main.layout {
-  display: flex;
-  flex-wrap: nowrap;
-  height: 100vh;
-  height: -webkit-fill-available;
-  max-height: 100vh;
-  overflow-x: auto;
-  overflow-y: hidden;
-
-  // Override styles when printing. Fixes issue where only the first page is visible when printing.
-  @media print {
-    position: static !important;
-    display: block !important;
-    height: 100%;
-    overflow-x: visible !important;
-    overflow-y: visible !important;
-  }
-}
-
-main.login-container {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  max-width: 100vw;
-  height: calc(100vh - 4rem);
-  padding-top: 40px;
-  padding-bottom: 40px;
-
-  + footer.footer button.color-mode-toggle {
-    color: var(--nbx-color-mode-toggle-color);
-  }
-}
-
-.footer {
-  background-color: $tab-content-bg;
-  padding: 0;
-  .nav-link {
-    padding: 0.5rem;
-  }
-
-  @include media-breakpoint-down(md) {
-    // Pad the bottom of the footer on mobile devices to account for mobile browser controls.
-    margin-bottom: 8rem;
-  }
-}
-
-footer.login-footer {
-  height: 4rem;
-  margin-top: auto;
-
-  .container-fluid {
-    display: flex;
-    justify-content: flex-end;
-    padding: $container-padding-x $grid-gutter-width;
-  }
-}
-
-h1.accordion-item-title,
-h2.accordion-item-title,
-h3.accordion-item-title,
-h4.accordion-item-title,
-h5.accordion-item-title,
-h6.accordion-item-title {
-  padding: 0.25rem 0.5rem;
-  font-size: $font-size-sm;
-  font-weight: $font-weight-bold;
-  color: var(--nbx-sidebar-title-color);
-  text-transform: uppercase;
-}
-
-.form-login {
-  width: 100%;
-  max-width: 330px;
-  padding: 15px;
-
-  input:focus {
-    z-index: 1;
-  }
-
-  input[type='text'] {
-    margin-bottom: -1px;
-    border-bottom-right-radius: 0;
-    border-bottom-left-radius: 0;
-  }
-  input[type='password'] {
-    margin-bottom: 10px;
-    border-top-left-radius: 0;
-    border-top-right-radius: 0;
-  }
-
-  .form-control {
-    position: relative;
-    box-sizing: border-box;
-    height: auto;
-    padding: 10px;
-    font-size: 16px;
-  }
-}
-
-.navbar {
-  border-bottom: 1px solid $border-color;
-}
-
-.navbar-brand {
-  padding-top: 0.75rem;
-  padding-bottom: 0.75rem;
-  font-size: 1rem;
-}
-
-nav.nav.nav-pills {
-  .nav-item.nav-link {
-    padding: 0.25rem 0.5rem;
-    font-size: $font-size-sm;
-    border-radius: $border-radius;
-
-    &:hover {
-      color: $accordion-button-active-color;
-      background-color: $accordion-button-active-bg;
-    }
-  }
-}
-
-// Ensure the content container is full-height, and that the content block is also full height so
-// that the footer is always at the bottom.
-div.content-container {
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  width: calc(100% - #{$sidenav-width-closed});
-  min-height: 100vh;
-  overflow-x: hidden;
-  overflow-y: auto;
-
-  // Don't show an outline when the content container is focused.
-  &:focus,
-  &:focus-visible {
-    outline: 0;
-  }
-
-  div.content {
-    background-color: $tab-content-bg;
-    flex: 1;
-  }
-
-  @include media-breakpoint-down(lg) {
-    width: 100%;
-  }
-
-  // Make the content full-width when printing.
-  @media print {
-    width: 100% !important;
-    margin-left: 0 !important;
-  }
-}
-
-// Prevent scrolling of body content when nav menu is open on mobile.
-.sidebar.collapse.show ~ .content-container > .content {
-  @media (max-width: map.get($grid-breakpoints, 'md')) {
-    position: fixed;
-    top: 0;
-    left: 0;
-    overflow-y: hidden;
-  }
-}
-
-.tooltip {
-  pointer-events: none;
-}
-
-span.color-label {
-  display: block;
-  width: 5rem;
-  height: 1rem;
-  padding: $badge-padding-y $badge-padding-x;
-  border: 1px solid #303030;
-  border-radius: $border-radius;
-  box-shadow: $box-shadow-sm;
-}
-
-.badge a {
-  color: inherit;
-}
-
-.btn {
-  white-space: nowrap;
-}
-
-.card {
-  box-shadow: $box-shadow-sm;
-
-  .card-header {
-    padding: $card-cap-padding-x;
-    color: var(--nbx-body-color);
-    border-bottom: none;
-  }
-
-  .card-header + .card-body {
-    padding-top: 0;
-  }
-
-  .card-body.small .form-control,
-  .card-body.small .form-select {
-    font-size: $input-font-size-sm;
-  }
-
-  .card-divider {
-    width: 100%;
-    height: 1px;
-    margin: $hr-margin-y 0;
-    border-top: 1px solid $card-border-color;
-    opacity: $hr-opacity;
-  }
-
-  // Remove shadow when printing.
-  @media print {
-    box-shadow: unset !important;
-  }
-}
-
-.form-floating {
-  position: relative;
-
-  > .input-group > .form-control,
-  > .input-group > .form-select {
-    height: $form-floating-height;
-    padding: $form-floating-padding-y $form-floating-padding-x;
-  }
-
-  > .input-group > label {
-    position: absolute;
-    top: 0;
-    left: 0;
-    height: 100%; // allow textareas
-    padding: $form-floating-padding-y $form-floating-padding-x;
-    pointer-events: none;
-    border: $input-border-width solid transparent; // Required for aligning label's text with the input as it affects inner box model
-    transform-origin: 0 0;
-    @include transition($form-floating-transition);
-  }
-
-  > .input-group > .form-control {
-    &::placeholder {
-      color: transparent;
-    }
-
-    &:focus,
-    &:not(:placeholder-shown) {
-      padding-top: $form-floating-input-padding-t;
-      padding-bottom: $form-floating-input-padding-b;
-    }
-    // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped
-    &:-webkit-autofill {
-      padding-top: $form-floating-input-padding-t;
-      padding-bottom: $form-floating-input-padding-b;
-    }
-  }
-
-  > .input-group > .form-select,
-  > .choices > .choices__inner,
-  > .ss-main span.placeholder, // SlimSelect Single
-  > .ss-main div.ss-values // SlimSelect Multiple
-  {
-    padding-top: $form-floating-input-padding-t;
-    padding-bottom: $form-floating-input-padding-b;
-  }
-
-  > .input-group > .form-control:focus,
-  > .input-group > .form-control:not(:placeholder-shown),
-  > .input-group > .form-select,
-  > .choices,
-  > .ss-main {
-    ~ label {
-      opacity: $form-floating-label-opacity;
-      transform: $form-floating-label-transform;
-      z-index: 4;
-    }
-  }
-  // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped
-  > .input-group > .form-control:-webkit-autofill {
-    ~ label {
-      z-index: 4;
-      opacity: $form-floating-label-opacity;
-      transform: $form-floating-label-transform;
-    }
-  }
-}
-
-.form-object-edit {
-  margin: 0 auto;
-  max-width: 800px;
-}
-
-textarea.form-control[rows='10'] {
-  height: 18rem;
-}
-
-textarea.markdown,
-textarea.form-control[name='csv'] {
-  font-family: $font-family-monospace;
-}
-
-.card:not(:only-of-type) {
-  margin-bottom: $spacer;
-}
-
-.stat-btn {
-  min-width: $spacer * 3;
-}
-
-nav.breadcrumb-container {
-  width: fit-content;
-  padding: $badge-padding-y $badge-padding-x;
-  font-size: $font-size-sm;
-
-  ol.breadcrumb {
-    margin-bottom: 0;
-    li.breadcrumb-item > a {
-      text-decoration: none;
-    }
-    li.breadcrumb-item > a:hover {
-      text-decoration: underline;
-    }
-  }
-}
-
-label.required {
-  font-weight: $font-weight-bold;
-
-  &:after {
-    position: absolute;
-    display: inline-block;
-    margin: 0 0 0 2px;
-    font-family: 'Material Design Icons';
-    font-size: 8px;
-    font-style: normal;
-    font-weight: $font-weight-medium;
-    text-decoration: none;
-    content: '\f06C4';
-  }
-}
-
-// Applied to containing element around table bulk-action buttons (bulk-edit, bulk-disconnect
-// bulk-delete, etc). Each usage of .bulk-buttons needs to have groups of buttons wrapped with
-// .bulk-button-group so that proper spacing is applied (even if there is only one group).
-div.bulk-buttons {
-  display: flex;
-  justify-content: space-between;
-  margin: math.div($spacer, 2) 0;
-
-  // Each group of buttons needs to be contained separately for alignment purposes. This way, you
-  // can put some buttons in a group that aligns left, and other buttons in a group that aligns
-  // right.
-  > div.bulk-button-group {
-    display: flex;
-    flex-wrap: wrap;
-    // For small screens: don't fill the available space (thereby expanding the size of the button).
-    align-items: flex-start;
-
-    &:first-of-type:not(:last-of-type) {
-      // If there are multiple bulk button groups and this is the first, the first button in the
-      // group should *not* have left spacing applied, so the button group aligns with the rest
-      // of the page elements.
-      > *:first-child {
-        margin-left: 0;
-      }
-    }
-
-    &:last-of-type:not(:first-of-type) {
-      // If there are multiple bulk button groups and this is the last, the last button in the
-      // group should *not* have right spacing applied, so the button group aligns with the rest
-      // of the page elements.
-      > *:last-child {
-        margin-right: 0;
-      }
-    }
-
-    // However, the rest of the buttons should have spacing applied in all directions.
-    > * {
-      margin: math.div($spacer, 4);
-    }
-  }
-}
-
-table tbody {
-  @each $color, $value in $theme-colors {
-    tr.#{$color} {
-      background-color: rgba($value, 0.15);
-      border-color: $gray-500;
-    }
-  }
-}
-
-// Style objects with statuses/roles within a table. As of implementation, used for IP addresses
-// assigned to interfaces.
-table .table-badge-group {
-  .table-badge {
-    display: block;
-    width: min-content;
-    font-size: $font-size-sm;
-    font-weight: $font-weight-base;
-
-    &:not(.badge) {
-      // Apply badge horizontal padding so that IP addresses *not* within a badge appear aligned
-      // with IP addresses that *are* within a badge.
-      padding: 0 $badge-padding-x;
-    }
-
-    &.badge:not(:last-of-type):not(:only-child) {
-      margin-bottom: map.get($spacers, 1);
-    }
-  }
-}
-
-pre.change-data {
-  padding-right: 0;
-  padding-left: 0;
-
-  > span {
-    display: block;
-    padding-right: $spacer;
-    padding-left: $spacer;
-
-    &.added {
-      background-color: var(--nbx-change-added);
-    }
-
-    &.removed {
-      background-color: var(--nbx-change-removed);
-    }
-  }
-}
-
-pre.change-diff {
-  border-color: transparent;
-
-  &.change-removed {
-    background-color: var(--nbx-change-removed);
-  }
-
-  &.change-added {
-    background-color: var(--nbx-change-added);
-  }
-}
-
-div.card-overlay {
-  position: absolute;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-  background-color: rgba($white, 0.75);
-  border-radius: $border-radius;
-
-  > div.spinner-border {
-    width: 6rem;
-    height: 6rem;
-    color: $secondary;
-  }
-}
-
-.table-controls {
-  display: flex;
-
-  @include media-breakpoint-up(md) {
-    // `!important` needed because of inherited margin-bottom from `.col`
-    margin-top: 0 !important;
-    margin-bottom: 0 !important;
-  }
-
-  .table-configure {
-    justify-content: flex-start;
-
-    @include media-breakpoint-up(md) {
-      justify-content: flex-end;
-    }
-  }
-
-  .form-switch.form-check-inline {
-    flex: 1 0 auto;
-    font-size: $font-size-sm;
-  }
-}
-
-// Tabbed content
-.nav-tabs {
-  background-color: $body-bg;
-  .nav-link {
-    &:hover {
-      // Don't show a bottom-border on a hovered nav link because it overlaps with the .nav-tab border.
-      border-bottom-color: transparent;
-    }
-    &.active {
-      // Set the background-color of an active tab to the same color as the .tab-content
-      // background-color so it visually indicates which tab is open.
-      background-color: $tab-content-bg;
-      border-bottom-color: $tab-content-bg;
-      // Move the active tab down 1px to overtake the .nav-tabs element's border, but only for that
-      // tab. This is an ugly hack, but it works.
-      transform: translateY(1px);
-    }
-  }
-}
-
-.tab-content {
-  display: flex;
-  flex-direction: column;
-  padding: $spacer;
-}
-
-// Override masonry-layout styles when printing.
-.masonry {
-  @media print {
-    position: static !important;
-    display: block !important;
-    height: unset !important;
-  }
-  .masonry-item {
-    @media print {
-      position: static !important;
-      top: unset !important;
-      left: unset !important;
-      display: block !important;
-    }
-  }
-}
-
-// Object hierarchy indicators.
-.record-depth {
-  display: inline;
-  font-size: $font-size-base;
-  user-select: none;
-  opacity: 0.33;
-
-  // Add spacing to the last or only dot.
-  span:only-of-type,
-  span:last-of-type {
-    margin-right: map.get($spacers, 1);
-  }
-}
-
-// Remove the max-width from image preview popovers as this is controlled on the image element.
-.popover.image-preview-popover {
-  max-width: unset;
-}
-
-/* Rendered Markdown */
-.rendered-markdown table {
-  width: 100%;
-}
-.rendered-markdown th {
-  border-bottom: 2px solid #dddddd;
-  padding: 8px;
-}
-.rendered-markdown td {
-  border-top: 1px solid #dddddd;
-  padding: 8px;
-}
-
-th[align="left"] {
-  text-align: left;
-}
-
-th[align="center"] {
-  text-align: center;
-}
-
-th[align="right"] {
-  text-align: right;
-}
-
-/* Markdown widget */
-.markdown-widget {
-  .nav-link {
-    border-bottom: 0;
-
-    &.active {
-      background-color: var(--nbx-body-bg);
-    }
-  }
-
-  .nav-tabs {
-    background-color: var(--nbx-pre-bg);
-  }
-}
-
-// Preformatted text blocks
-td pre {
-  margin-bottom: 0;
-}
-pre.block {
-  padding: $spacer;
-  background-color: var(--nbx-pre-bg);
-  border: 1px solid var(--nbx-pre-border-color);
-  border-radius: $border-radius;
-}
-
-#django-messages {
-  position: fixed;
-  right: $spacer;
-  bottom: 0;
-  margin: $spacer;
-}
-
-// Page-specific styles.
-html {
-  // Shade the home page content background-color.
-  &[data-netbox-url-name='home'] {
-    .content-container,
-    .search {
-      background-color: $gray-100 !important;
-    }
-    &[data-netbox-color-mode='dark'] {
-      .content-container,
-      .search {
-        background-color: $darkest !important;
-      }
-    }
-  }
-
-  // Don't show the django-messages toasts on the login screen in favor of the alert component.
-  &[data-netbox-url-name='login'] {
-    #django-messages {
-      display: none;
-    }
-  }
-}
+@import 'variables';
+
+// Tabler
+@import '../node_modules/@tabler/core/src/scss/_core.scss';
+
+// Overrides of external libraries
+@import 'overrides/slim-select';
+@import 'overrides/tabler';
+
+// Transitional styling to ease migration of templates from NetBox v3.x
+@import 'transitional/badges';
+@import 'transitional/cards';
+@import 'transitional/forms';
+@import 'transitional/layout';
+@import 'transitional/navigation';
+@import 'transitional/progress';
+@import 'transitional/tables';
+@import 'transitional/tabs';
+
+// Custom styling
+@import 'custom/code';
+@import 'custom/markdown';
+@import 'custom/misc';
diff --git a/netbox/project-static/styles/theme-light.scss b/netbox/project-static/styles/old/theme-light.scss
similarity index 52%
rename from netbox/project-static/styles/theme-light.scss
rename to netbox/project-static/styles/old/theme-light.scss
index c9478f1cc..9b861c3a5 100644
--- a/netbox/project-static/styles/theme-light.scss
+++ b/netbox/project-static/styles/old/theme-light.scss
@@ -1,6 +1,58 @@
-// Light Mode Theme Variables and Overrides.
+// Base NetBox Theme Overrides and Settings - color mode agnostic.
 
-@import './theme-base.scss';
+@import '../../node_modules/bootstrap/scss/functions';
+
+$card-cap-bg: 'unset';
+
+$border-radius-md: 0.375rem;
+$border-radius-lg: 0.5rem;
+$border-radius-xl: 0.75rem;
+$border-radius-2xl: 1.5rem;
+
+$border-radius: $border-radius-md;
+
+$border-radius-sm: $border-radius;
+$border-radius-lg: $border-radius-xl;
+
+$badge-border-radius: $border-radius-md;
+$progress-border-radius: $border-radius-md;
+
+$font-weight-lighter: 200;
+$font-weight-medium: 600;
+$font-weight-bolder: 800;
+
+$font-size-xs: 0.75rem;
+
+$line-height-base: 1.5;
+$line-height-xs: 1;
+$line-height-sm: 1.25;
+$line-height-lg: 1.75;
+
+$darker: #1b1f22;
+$darkest: #171b1d;
+
+@import '../../node_modules/bootstrap/scss/variables';
+@import '../../node_modules/bootstrap/scss/variables-dark';
+
+// This is the same value as the default from Bootstrap, but it needs to be in scope prior to
+// importing _variables.scss from Bootstrap.
+$btn-close-width: 1em;
+
+$accordion-padding-y: 0.8125rem;
+$accordion-padding-x: 0.8125rem;
+
+$sidebar-width: 280px;
+$sidebar-bottom-height: 4rem;
+
+$transition-100ms-ease-in-out: all 0.1s ease-in-out;
+
+// Sidebar/Sidenav
+$sidenav-width-closed: 3rem;
+$sidenav-width-open: 16rem;
+$sidenav-icon-width: 2rem;
+$sidenav-spacing-x: 1.5rem;
+$sidenav-link-spacing-x: 1rem;
+$sidenav-link-spacing-y: 0.5rem;
 
 // Theme colors (BS5 classes)
 $primary: #337ab7;
diff --git a/netbox/project-static/styles/overrides.scss b/netbox/project-static/styles/overrides.scss
deleted file mode 100644
index e7643eaea..000000000
--- a/netbox/project-static/styles/overrides.scss
+++ /dev/null
@@ -1,50 +0,0 @@
-// Overrides to native HTML elements (i.e., not bootstrap classes or custom components).
-
-body {
-  color: var(--nbx-body-color);
-  background-color: var(--nbx-body-bg);
-  font-size: $font-size-sm;
-}
-
-pre {
-  white-space: pre;
-}
-
-// Force  elements to make text smaller.
-small {
-  font-size: smaller !important;
-}
-
-// Fix the hideous way Safari shows button anchor elements.
-a[type='button'] {
-  -webkit-appearance: unset !important;
-}
-
-// Ensure elements with data-href set show the correct cursor.
-// data-href is set on non non-anchor elements that need to redirect the user to a URL when
-// clicked, but where an anchor element does not suffice or is not supported.
-*[data-href] {
-  cursor: pointer;
-}
-
-.form-control:not([type='file']) {
-  font-size: inherit;
-}
-
-.badge {
-  font-size: $font-size-xs;
-}
-
-/* clears the 'X' in search inputs from webkit browsers */
-input[type='search']::-webkit-search-decoration,
-input[type='search']::-webkit-search-cancel-button,
-input[type='search']::-webkit-search-results-button,
-input[type='search']::-webkit-search-results-decoration {
-  -webkit-appearance: none !important;
-}
-
-// Remove x-axis padding from highlighted text
-mark {
-  padding-left: 0;
-  padding-right: 0;
-}
\ No newline at end of file
diff --git a/netbox/project-static/styles/select.scss b/netbox/project-static/styles/overrides/_slim-select.scss
similarity index 93%
rename from netbox/project-static/styles/select.scss
rename to netbox/project-static/styles/overrides/_slim-select.scss
index d8fcd218e..e9f079d44 100644
--- a/netbox/project-static/styles/select.scss
+++ b/netbox/project-static/styles/overrides/_slim-select.scss
@@ -37,16 +37,6 @@ $spacing-s: $input-padding-x;
 .ss-main {
   color: $form-select-color;
 
-  &.is-invalid .ss-single-selected,
-  &.is-invalid .ss-multi-selected {
-    border-color: $form-feedback-icon-invalid-color;
-  }
-
-  &.is-valid .ss-single-selected,
-  &.is-valid .ss-multi-selected {
-    border-color: $form-feedback-icon-valid-color;
-  }
-
   .ss-single-selected,
   .ss-multi-selected {
     padding: $form-select-padding-y $input-padding-x $form-select-padding-y $form-select-padding-x;
@@ -184,3 +174,22 @@ $spacing-s: $input-padding-x;
     }
   }
 }
+
+// Fix slim-select 1.x placeholder styling
+.ss-main {
+	.ss-single-selected {
+		.placeholder {
+			cursor: pointer;
+			opacity: 1;
+			background-color: transparent !important;
+		}
+	}
+}
+
+// Apply red border for fields inside a row with .has-errors
+.has-errors {
+  .ss-single-selected,
+  .ss-multi-selected {
+    border-color: $red;
+  }
+}
diff --git a/netbox/project-static/styles/overrides/_tabler.scss b/netbox/project-static/styles/overrides/_tabler.scss
new file mode 100644
index 000000000..a5ae3c647
--- /dev/null
+++ b/netbox/project-static/styles/overrides/_tabler.scss
@@ -0,0 +1,25 @@
+// Restore default foreground & background colors for 
 blocks
+pre {
+  background-color: transparent;
+  color: inherit;
+}
+
+// Buttons
+.btn {
+  // Tabler sets display: flex
+  display: inline-block;
+}
+
+// Tabs
+.nav-tabs {
+  .nav-link {
+    // Tabler sets display: flex
+    display: inline-block;
+  }
+}
+
+// Dropdown items
+.dropdown-item {
+  // Tabler sets display: flex
+  display: inline-block;;
+}
diff --git a/netbox/project-static/styles/sidenav.scss b/netbox/project-static/styles/sidenav.scss
deleted file mode 100644
index 3e2a53ef9..000000000
--- a/netbox/project-static/styles/sidenav.scss
+++ /dev/null
@@ -1,450 +0,0 @@
-@use 'sass:map';
-@use 'sass:math';
-
-@mixin parent-link {
-  .navbar-nav .nav-item .nav-link[data-bs-toggle] {
-    @content;
-  }
-}
-
-@mixin child-link {
-  .collapse .nav .nav-item .nav-link {
-    @content;
-  }
-}
-
-@mixin sidenav-open {
-  body[data-sidenav-show],
-  body[data-sidenav-pinned] {
-    .sidenav {
-      @content;
-    }
-  }
-}
-
-@mixin sidenav-closed {
-  body[data-sidenav-hide],
-  body[data-sidenav-hidden] {
-    .sidenav {
-      @content;
-    }
-  }
-}
-
-@mixin sidenav-pinned {
-  body[data-sidenav-pinned] {
-    .sidenav {
-      @content;
-    }
-  }
-}
-
-@mixin sidenav-show {
-  body[data-sidenav-show] {
-    .sidenav {
-      @content;
-    }
-  }
-}
-
-@mixin sidenav-hide {
-  body[data-sidenav-hide] {
-    .sidenav {
-      @content;
-    }
-  }
-}
-
-@mixin sidenav-peek {
-  .g-sidenav-show:not(.g-sidenav-pinned) {
-    .sidenav {
-      @content;
-    }
-  }
-}
-
-.sidenav {
-  position: fixed;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  z-index: 1050;
-  display: block;
-  width: 100%;
-  max-width: $sidenav-width-closed;
-  padding-top: 0;
-  padding-right: 0;
-  padding-left: 0;
-  background-color: var(--nbx-sidebar-bg);
-  border-right: 1px solid $border-color;
-  transition: $transition-100ms-ease-in-out;
-
-  // Media fixes for mobile resolutions.
-  @include media-breakpoint-down(lg) {
-    transform: translateX(-$sidenav-width-closed);
-
-    + .content-container[class] {
-      margin-left: 0;
-    }
-
-    .profile-button-container[class] {
-      display: block;
-    }
-  }
-
-  .profile-button-container {
-    display: none;
-    padding: $sidenav-link-spacing-y $sidenav-link-spacing-x;
-  }
-
-  + .content-container {
-    margin-left: $sidenav-width-closed;
-    transition: $transition-100ms-ease-in-out;
-  }
-
-  // Navbar brand
-  .sidenav-brand {
-    margin-right: 0;
-    transition: opacity 0.1s ease-in-out;
-  }
-
-  .sidenav-brand-icon {
-    transition: opacity 0.1s ease-in-out;
-  }
-
-  .sidenav-inner {
-    padding-right: $sidenav-spacing-x;
-    padding-left: $sidenav-spacing-x;
-    @include media-breakpoint-up(md) {
-      padding-right: 0;
-      padding-left: 0;
-    }
-  }
-
-  .sidenav-brand-img,
-  .sidenav-brand > img {
-    max-width: 100%;
-    max-height: calc(#{$sidenav-width-open} - 1rem);
-  }
-
-  .navbar-heading {
-    padding-top: $nav-link-padding-y;
-    padding-bottom: $nav-link-padding-y;
-    font-size: $font-size-xs;
-    text-transform: uppercase;
-    letter-spacing: 0.04em;
-  }
-
-  .sidenav-header {
-    position: relative;
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    height: 78px;
-    padding: $spacer;
-    transition: $transition-100ms-ease-in-out;
-  }
-
-  .sidenav-toggle {
-    // The sidenav toggle's default state is "hidden". Because modifying the `display` property
-    // isn't ideal for smooth transitions, combine opacity 0 (transparent) and position absolute
-    // to yield a similar result.
-    position: absolute;
-    display: inline-block;
-    opacity: 0;
-    // The transition itself is largely irrelevant, but CSS needs *something* to transition in
-    // order to apply a delay.
-    transition: opacity 10ms ease-in-out;
-    // Offset the transition delay so the icon isn't visible during the logo transition.
-    transition-delay: 0.1s;
-  }
-
-  .sidenav-collapse {
-    display: flex;
-    flex: 1;
-    flex-direction: column;
-    align-items: stretch;
-    padding-right: $sidenav-spacing-x;
-    padding-left: $sidenav-spacing-x;
-    margin-right: -$sidenav-spacing-x;
-    margin-left: -$sidenav-spacing-x;
-
-    > * {
-      min-width: 100%;
-    }
-
-    @include media-breakpoint-up(md) {
-      margin-right: 0;
-      margin-left: 0;
-    }
-  }
-
-  .nav-group-header {
-    padding: math.div($sidenav-link-spacing-y, 2) $sidenav-link-spacing-x;
-    margin-top: 0.5rem;
-    margin-bottom: 0;
-  }
-
-  .nav {
-    margin-bottom: 0.5rem;
-  }
-
-  // Child Link nav-item
-  .nav .nav-item {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    width: 100%;
-
-    &.no-buttons {
-      padding-right: 0.5rem;
-    }
-  }
-
-  @include child-link() {
-    width: 100%;
-    padding-top: math.div($sidenav-link-spacing-y, 2);
-    padding-right: map.get($spacers, 1);
-    padding-bottom: math.div($sidenav-link-spacing-y, 2);
-    padding-left: $sidenav-link-spacing-x;
-    margin-top: 0;
-    margin-bottom: 0;
-    border-top-right-radius: $border-radius;
-    border-bottom-right-radius: $border-radius;
-
-    .sidenav-mini-icon {
-      width: $sidenav-link-spacing-x;
-      text-align: center;
-      transition: $transition-100ms-ease-in-out;
-    }
-  }
-
-  @include parent-link() {
-    width: unset;
-    height: 100%;
-    padding-left: math.div($sidenav-link-spacing-x, 2);
-    font-weight: $font-weight-bold;
-    color: var(--nbx-sidenav-parent-color);
-
-    &:after {
-      display: inline-block;
-      margin-left: auto;
-      /* stylelint-disable */
-      font-family: 'Material Design Icons';
-      /* stylelint-enable */
-      font-style: normal;
-      font-weight: 700;
-      font-variant: normal;
-      color: $text-muted;
-      text-rendering: auto;
-      -webkit-font-smoothing: antialiased;
-      content: '\f0142';
-      transition: $transition-100ms-ease-in-out;
-    }
-
-    // Expanded
-    &[aria-expanded='true'] {
-      &.active:after {
-        color: $accordion-button-active-color;
-      }
-      &:after {
-        color: $primary;
-        transform: rotate(90deg);
-      }
-    }
-
-    .nav-link-text {
-      padding-left: 0.25rem;
-      transition: $transition-100ms-ease-in-out;
-    }
-  }
-
-  .navbar-nav {
-    flex-direction: column;
-    margin-right: -$sidenav-spacing-x;
-    margin-left: -$sidenav-spacing-x;
-
-    .nav-item {
-
-      &.disabled {
-        cursor: not-allowed;
-        opacity: 0.8;
-      }
-
-      // All Links
-      .nav-link {
-        position: relative;
-        display: flex;
-        align-items: center;
-        width: 100%;
-        padding: $sidenav-link-spacing-y $sidenav-link-spacing-x;
-        font-size: $font-size-sm;
-        color: var(--nbx-sidenav-link-color);
-        white-space: nowrap;
-        transition-duration: 0ms;
-
-        &.active {
-          background-color: var(--nbx-sidebar-link-active-bg);
-        }
-
-        &:hover:not(.active) {
-          color: var(--nbx-body-color);
-          background-color: var(--nbx-sidebar-link-hover-bg);
-        }
-
-        // Icon
-        > i {
-          min-width: $sidenav-icon-width;
-          font-size: calc(45px / 2);
-          text-align: center;
-        }
-      }
-    }
-
-    .nav-group-label {
-      display: block;
-      font-size: $font-size-xs;
-      font-weight: $font-weight-bold;
-      // color: $secondary;
-      color: var(--nbx-sidenav-group-color);
-      text-transform: uppercase;
-      white-space: nowrap;
-    }
-  }
-}
-
-@include sidenav-pinned() {
-  .sidenav-toggle-icon {
-    color: var(--nbx-sidenav-pin-color);
-    transform: rotate(90deg);
-  }
-  @include media-breakpoint-up(xl) {
-    + .content-container {
-      margin-left: $sidenav-width-open;
-    }
-  }
-}
-
-@include sidenav-peek() {
-  .sidenav-toggle-icon {
-    transform: rotate(0deg);
-  }
-}
-
-@include sidenav-open() {
-  max-width: $sidenav-width-open;
-
-  .sidenav-brand,
-  .navbar-heading {
-    display: block;
-  }
-
-  .sidenav-brand {
-    opacity: 1;
-    transform: translateX(0);
-  }
-
-  .sidenav-brand-icon {
-    position: absolute;
-    opacity: 0;
-  }
-
-  @include media-breakpoint-down(lg) {
-    transform: translateX(0);
-  }
-}
-
-@include sidenav-closed() {
-  .sidenav-header {
-    padding: $spacer * 0.5;
-  }
-
-  .sidenav-brand {
-    position: absolute;
-    opacity: 0;
-  }
-
-  .sidenav-brand-icon {
-    opacity: 1;
-  }
-
-  .sidenav-toggle {
-    // Immediately hide the toggle when the sidenav is closed, so it doesn't linger and overlap
-    // with the logo elements.
-    opacity: 0;
-    position: absolute;
-    transition: unset;
-    transition-delay: 0ms;
-  }
-
-  .navbar-nav > .nav-item {
-    > .nav-link {
-      &:after {
-        content: '';
-      }
-    }
-  }
-
-  .nav-item .collapse {
-    display: none;
-  }
-
-  .nav-link-text {
-    opacity: 0;
-  }
-
-  @include parent-link() {
-    &.active {
-      margin-right: 0;
-      margin-left: 0;
-      border-radius: unset;
-    }
-  }
-}
-
-@include sidenav-show() {
-  .sidenav-brand {
-    display: block;
-  }
-
-  .nav-item .collapse {
-    height: auto;
-    transition: $transition-100ms-ease-in-out;
-  }
-
-  .nav-item .nav-link .nav-link-text {
-    opacity: 1;
-  }
-
-  .nav-item .sidenav-mini-icon {
-    opacity: 0;
-  }
-
-  @include media-breakpoint-up(lg) {
-    .sidenav-toggle {
-      position: relative;
-      opacity: 1;
-    }
-  }
-}
-
-.simplebar-track.simplebar-vertical {
-  right: 0;
-  width: 6px;
-  background-color: transparent;
-
-  .simplebar-scrollbar {
-    transition: none;
-
-    &:before {
-      right: 0;
-      width: 3px;
-      background: var(--nbx-sidebar-scroll);
-      border-radius: $border-radius;
-    }
-  }
-
-  &.simplebar-hover .simplebar-scrollbar:before {
-    width: 5px;
-  }
-}
diff --git a/netbox/project-static/styles/cable-trace.scss b/netbox/project-static/styles/svg/cable_trace.scss
similarity index 98%
rename from netbox/project-static/styles/cable-trace.scss
rename to netbox/project-static/styles/svg/cable_trace.scss
index 59c67ad4d..4a8fdf61a 100644
--- a/netbox/project-static/styles/cable-trace.scss
+++ b/netbox/project-static/styles/svg/cable_trace.scss
@@ -1,3 +1,5 @@
+@import '../old/theme-light';
+
 // Cable Trace Styles.
 
 :root {
@@ -73,3 +75,4 @@ svg {
     stroke-dasharray: 5px;
   }
 }
+
diff --git a/netbox/project-static/styles/rack-elevation.scss b/netbox/project-static/styles/svg/rack_elevation.scss
similarity index 98%
rename from netbox/project-static/styles/rack-elevation.scss
rename to netbox/project-static/styles/svg/rack_elevation.scss
index 8d6bdddb9..424dd823e 100644
--- a/netbox/project-static/styles/rack-elevation.scss
+++ b/netbox/project-static/styles/svg/rack_elevation.scss
@@ -1,3 +1,5 @@
+@import '../old/theme-light';
+
 // Rack Elevation Styles.
 
 svg {
@@ -110,3 +112,4 @@ svg {
     fill: url(#reserved);
   }
 }
+
diff --git a/netbox/project-static/styles/theme-base.scss b/netbox/project-static/styles/theme-base.scss
deleted file mode 100644
index 97f6dd020..000000000
--- a/netbox/project-static/styles/theme-base.scss
+++ /dev/null
@@ -1,54 +0,0 @@
-// Base NetBox Theme Overrides and Settings - color mode agnostic.
-
-@import '../node_modules/bootstrap/scss/functions';
-
-$card-cap-bg: 'unset';
-
-$border-radius-md: 0.375rem;
-$border-radius-lg: 0.5rem;
-$border-radius-xl: 0.75rem;
-$border-radius-2xl: 1.5rem;
-
-$border-radius: $border-radius-md;
-
-$border-radius-sm: $border-radius;
-$border-radius-lg: $border-radius-xl;
-
-$badge-border-radius: $border-radius-md;
-$progress-border-radius: $border-radius-md;
-
-$font-weight-lighter: 200;
-$font-weight-medium: 600;
-$font-weight-bolder: 800;
-
-$font-size-xs: 0.75rem;
-
-$line-height-base: 1.5;
-$line-height-xs: 1;
-$line-height-sm: 1.25;
-$line-height-lg: 1.75;
-
-$darker: #1b1f22;
-$darkest: #171b1d;
-
-@import '../node_modules/bootstrap/scss/variables';
-
-// This is the same value as the default from Bootstrap, but it needs to be in scope prior to
-// importing _variables.scss from Bootstrap.
-$btn-close-width: 1em;
-
-$accordion-padding-y: 0.8125rem;
-$accordion-padding-x: 0.8125rem;
-
-$sidebar-width: 280px;
-$sidebar-bottom-height: 4rem;
-
-$transition-100ms-ease-in-out: all 0.1s ease-in-out;
-
-// Sidebar/Sidenav
-$sidenav-width-closed: 3rem;
-$sidenav-width-open: 16rem;
-$sidenav-icon-width: 2rem;
-$sidenav-spacing-x: 1.5rem;
-$sidenav-link-spacing-x: 1rem;
-$sidenav-link-spacing-y: 0.5rem;
diff --git a/netbox/project-static/styles/theme-dark.scss b/netbox/project-static/styles/theme-dark.scss
deleted file mode 100644
index 4bbe5cea5..000000000
--- a/netbox/project-static/styles/theme-dark.scss
+++ /dev/null
@@ -1,291 +0,0 @@
-// Dark Mode Theme Variables and Overrides.
-
-@use 'sass:map';
-@import './theme-base';
-
-// Theme colors (BS5 classes)
-$primary: $blue-300;
-$secondary: $gray-500;
-$success: $green-300;
-$info: $cyan-300;
-$warning: $yellow-300;
-$danger: $red-300;
-$light: $gray-300;
-$dark: $gray-500;
-
-$theme-colors: (
-  // BS5 theme colors
-  'primary': $primary,
-  'secondary': $secondary,
-  'success': $success,
-  'info': $info,
-  'warning': $warning,
-  'danger': $danger,
-  'light': $light,
-  'dark': $dark,
-  // General-purpose palette
-  'blue': $blue-300,
-  'indigo': $indigo-300,
-  'purple': $purple-300,
-  'pink': $pink-300,
-  'red': $red-300,
-  'orange': $orange-300,
-  'yellow': $yellow-300,
-  'green': $green-300,
-  'teal': $teal-300,
-  'cyan': $cyan-300,
-  'gray': $gray-300,
-  'black': $black,
-  'white': $white
-);
-
-// Gradient
-$gradient: linear-gradient(180deg, rgba($white, 0.15), rgba($white, 0));
-
-// Body
-$body-bg: $darker;
-$body-color: $white;
-$body-text-align: null;
-$border-color: $gray-700;
-$box-shadow: 0 0.5rem 1rem rgba($black, 0.15);
-$box-shadow-sm: 0 0.125rem 0.25rem rgba($black, 0.075);
-$box-shadow-lg: 0 1rem 3rem rgba($black, 0.175);
-$box-shadow-inset: inset 0 1px 2px rgba($black, 0.075);
-$text-muted: $gray-400;
-$blockquote-footer-color: $gray-600;
-$mark-bg: #fcf8e3;
-$link-color: $blue-200;
-$link-hover-color: $blue-100;
-
-// Alerts
-$alert-bg-scale: -5%;
-$alert-border-scale: -20%;
-$alert-color-scale: 20%;
-
-// Tables
-$table-color: $gray-100;
-$table-border-color: $border-color;
-$table-striped-color: $table-color;
-$table-striped-bg: rgba($white, $table-striped-bg-factor);
-$table-active-color: $table-color;
-$table-active-bg: rgba($white, $table-active-bg-factor);
-$table-hover-color: $table-color;
-$table-hover-bg: rgba($white, $table-hover-bg-factor);
-$table-flush-header-bg: $gray-700;
-
-// Buttons
-$btn-box-shadow: inset 0 1px 0 rgba($black, 0.15), 0 1px 1px rgba($white, 0.075);
-$btn-active-box-shadow: inset 0 3px 5px rgba($white, 0.125);
-$btn-link-disabled-color: $gray-300;
-
-// Forms
-$component-active-bg: $primary;
-$component-active-color: $black;
-$form-text-color: $text-muted;
-$input-bg: $gray-900;
-$input-disabled-bg: $gray-700;
-$input-color: $gray-100;
-$input-border-color: $gray-700;
-$input-focus-bg: $input-bg;
-$input-focus-border-color: tint-color($component-active-bg, 10%);
-$input-focus-color: $input-color;
-$input-placeholder-color: $gray-700;
-$input-plaintext-color: $body-color;
-
-input {
-  color-scheme: dark;
-}
-
-$form-check-input-active-filter: brightness(90%);
-$form-check-input-bg: $input-bg;
-$form-check-input-border: 1px solid rgba(255, 255, 255, 0.25);
-$form-check-input-checked-color: $component-active-color;
-$form-check-input-checked-bg-color: $component-active-bg;
-$form-check-input-checked-border-color: $form-check-input-checked-bg-color;
-$form-check-input-indeterminate-color: $component-active-color;
-$form-check-input-indeterminate-bg-color: $component-active-bg;
-$form-check-input-indeterminate-border-color: $form-check-input-indeterminate-bg-color;
-
-$form-switch-color: rgba(255, 255, 255, 0.25);
-$form-switch-focus-color: $input-focus-border-color;
-$form-switch-checked-color: $component-active-color;
-
-$input-group-addon-color: $input-color;
-$input-group-addon-bg: $gray-700;
-$input-group-addon-border-color: $input-border-color;
-
-$form-select-color: $input-color;
-$form-select-disabled-color: $gray-500;
-$form-select-bg: $input-bg;
-$form-select-disabled-bg: $input-disabled-bg;
-$form-select-indicator-color: $form-select-color;
-$form-select-indicator: url("data:image/svg+xml,");
-
-$form-select-border-color: $input-border-color;
-$form-range-track-bg: $gray-300;
-
-$form-range-thumb-bg: $component-active-bg;
-$form-range-thumb-box-shadow: 0 0.1rem 0.25rem rgba($black, 0.1);
-$form-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow;
-$form-range-thumb-active-bg: tint-color($component-active-bg, 70%);
-$form-range-thumb-disabled-bg: $gray-500;
-
-$form-file-button-color: $input-color;
-$form-file-button-bg: $input-group-addon-bg;
-$form-file-button-hover-bg: shade-color($form-file-button-bg, 5%);
-
-$form-feedback-valid-color: $success;
-$form-feedback-invalid-color: $danger;
-
-// Navs
-$nav-link-color: $body-color;
-$nav-link-hover-color: null;
-$nav-link-disabled-color: $gray-800;
-$nav-tabs-border-color: $border-color;
-$nav-tabs-link-hover-border-color: rgba($gray-800, 0.5) rgba($gray-800, 0.5) $nav-tabs-border-color;
-$nav-tabs-link-active-color: $gray-100;
-$nav-tabs-link-active-bg: $body-bg;
-$nav-tabs-link-active-border-color: $gray-800 $gray-800 $nav-tabs-link-active-bg;
-$nav-pills-link-active-color: $component-active-color;
-$nav-pills-link-active-bg: $component-active-bg;
-
-$navbar-light-color: $darker;
-$navbar-light-toggler-border-color: $gray-700;
-$navbar-light-toggler-icon-bg: url("data:image/svg+xml,");
-
-// Dropdowns
-$dropdown-color: $body-color;
-$dropdown-bg: $gray-900;
-$dropdown-border-color: rgba($white, 0.15);
-$dropdown-link-color: $gray-100;
-$dropdown-link-hover-color: $white;
-$dropdown-link-hover-bg: $gray-600;
-$dropdown-link-disabled-color: $gray-800;
-$dropdown-header-color: $gray-600;
-
-// Pagination
-$pagination-color: $link-color;
-$pagination-bg: $gray-800;
-$pagination-border-color: $gray-600;
-$pagination-focus-color: $link-hover-color;
-$pagination-focus-bg: $gray-400;
-$pagination-hover-color: $link-hover-color;
-$pagination-hover-bg: $gray-400;
-$pagination-hover-border-color: $gray-500;
-$pagination-active-color: $component-active-color;
-$pagination-active-bg: $component-active-bg;
-$pagination-active-border-color: $pagination-active-bg;
-$pagination-disabled-color: $gray-600;
-$pagination-disabled-bg: $gray-800;
-$pagination-disabled-border-color: $gray-600;
-
-// Cards
-$card-border-color: rgba($white, 0.125);
-$card-inner-border-radius: subtract($card-border-radius, $card-border-width);
-
-$card-cap-color: null;
-$card-height: null;
-$card-color: null;
-$card-bg: $gray-900;
-
-// Accordion
-$accordion-color: $body-color;
-$accordion-bg: transparent;
-$accordion-border-color: $border-color;
-$accordion-button-color: $accordion-color;
-$accordion-button-bg: $accordion-bg;
-$accordion-button-active-bg: shade-color($blue-300, 10%);
-$accordion-button-active-color: color-contrast($accordion-button-active-bg);
-$accordion-button-focus-border-color: $input-focus-border-color;
-$accordion-icon-color: $accordion-color;
-$accordion-icon-active-color: $accordion-button-active-color;
-$accordion-button-icon: url("data:image/svg+xml,");
-$accordion-button-active-icon: url("data:image/svg+xml,");
-
-// Tooltips
-$tooltip-color: $body-color;
-$tooltip-bg: $gray-700;
-$tooltip-arrow-color: $tooltip-bg;
-$form-feedback-tooltip-opacity: $tooltip-opacity;
-
-// Popovers
-$popover-bg: $gray-700;
-$popover-border-color: rgba($white, 0.2);
-$popover-header-bg: shade-color($popover-bg, 6%);
-$popover-header-color: $headings-color;
-$popover-body-color: $body-color;
-$popover-arrow-color: $popover-bg;
-$popover-arrow-outer-color: fade-in($popover-border-color, 0.05);
-
-// Toasts
-$toast-color: null;
-$toast-background-color: rgba($white, 0.85);
-$toast-border-color: rgba(0, 0, 0, 0.1);
-$toast-header-color: $gray-600;
-$toast-header-background-color: rgba($white, 0.85);
-$toast-header-border-color: rgba(0, 0, 0, 0.05);
-
-// Badges
-$badge-color: $white;
-
-// Modals
-$modal-content-color: null;
-$modal-content-bg: $gray-800;
-$modal-content-border-color: rgba($white, 0.2);
-$modal-backdrop-bg: $black;
-$modal-header-border-color: $border-color;
-$modal-footer-border-color: $modal-header-border-color;
-
-// Progress bars
-$progress-bg: $gray-600;
-$progress-bar-color: $white;
-$progress-bar-bg: $primary;
-
-// List group
-$list-group-color: $body-color;
-$list-group-bg: $card-bg;
-$list-group-border-color: rgba($white, 0.125);
-$list-group-hover-bg: rgba($gray-100, 0.15);
-$list-group-active-color: $component-active-color;
-$list-group-active-bg: $component-active-bg;
-$list-group-active-border-color: $list-group-active-bg;
-$list-group-disabled-bg: $list-group-bg;
-$list-group-action-color: $gray-300;
-$list-group-action-hover-color: $body-color;
-$list-group-action-active-color: $body-color;
-$list-group-action-active-bg: rgba($gray-300, 0.125);
-
-// Image thumbnails
-$thumbnail-bg: $body-bg;
-$thumbnail-border-color: $gray-300;
-
-// Figures
-$figure-caption-color: $gray-600;
-
-// Breadcrumbs
-$breadcrumb-divider-color: $gray-100;
-$breadcrumb-active-color: $body-color;
-$breadcrumb-divider-flipped: $breadcrumb-divider;
-$breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='#{$breadcrumb-divider-color}'/%3E%3C/svg%3E");
-
-// Carousel
-$carousel-control-color: $white;
-$carousel-indicator-active-bg: $white;
-$carousel-caption-color: $white;
-$carousel-dark-indicator-active-bg: $black;
-$carousel-dark-caption-color: $black;
-$carousel-dark-control-icon-filter: invert(1) grayscale(100);
-
-// Close
-$btn-close-color: $white;
-$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
-$btn-close-bg: url("data:image/svg+xml,");
-
-// Code
-$code-color: $gray-200;
-$kbd-color: $white;
-$kbd-bg: $gray-300;
-$pre-color: null;
-
-// Tabbed content
-$tab-content-bg: $darkest;
diff --git a/netbox/project-static/styles/transitional/_badges.scss b/netbox/project-static/styles/transitional/_badges.scss
new file mode 100644
index 000000000..10d03224b
--- /dev/null
+++ b/netbox/project-static/styles/transitional/_badges.scss
@@ -0,0 +1,7 @@
+// Links inside badges should inherit their foreground color
+.badge {
+  a {
+    color: inherit;
+    text-decoration: none;
+  }
+}
diff --git a/netbox/project-static/styles/transitional/_cards.scss b/netbox/project-static/styles/transitional/_cards.scss
new file mode 100644
index 000000000..6a7666524
--- /dev/null
+++ b/netbox/project-static/styles/transitional/_cards.scss
@@ -0,0 +1,30 @@
+.page-body {
+  .card {
+
+    // Add bottom margin for vertical separation between cards
+    margin-bottom: 1rem;
+
+    // Reduce padding
+    .card-header,
+    .card-body,
+    .card-footer {
+      padding: 0.75rem;
+    }
+
+    // Add background color
+    .card-header {
+      background: var(--#{$prefix}bg-surface-tertiary);
+    }
+
+    // Reduce padding
+    .list-group-item {
+      padding: 0.5rem 0.75rem;
+    }
+
+    // Remove bottom margin
+    .table {
+      margin-bottom: 0;
+    }
+
+  }
+}
diff --git a/netbox/project-static/styles/transitional/_forms.scss b/netbox/project-static/styles/transitional/_forms.scss
new file mode 100644
index 000000000..a489d87de
--- /dev/null
+++ b/netbox/project-static/styles/transitional/_forms.scss
@@ -0,0 +1,27 @@
+// Limit the width of object edit forms
+form.object-edit {
+  margin: auto;
+  max-width: 800px;
+}
+
+// Set bond font & append an asterisk to labels for required fields
+.col-form-label.required {
+  font-weight: bold;
+  &:after {
+    position: absolute;
+    display: inline-block;
+    margin-left: 0;
+    font-family: 'Material Design Icons';
+    font-size: 8px;
+    content: '\f06C4';
+  }
+}
+
+// Set red border on form fields inside a row with .has-errors
+.has-errors {
+  input,
+  select,
+  textarea {
+    border: 1px solid $red;
+  }
+}
diff --git a/netbox/project-static/styles/transitional/_layout.scss b/netbox/project-static/styles/transitional/_layout.scss
new file mode 100644
index 000000000..e1dd57b5b
--- /dev/null
+++ b/netbox/project-static/styles/transitional/_layout.scss
@@ -0,0 +1,8 @@
+// Tweak layout section colors
+.page {
+  background-color: var(--#{$prefix}bg-surface-secondary);
+}
+.page-header {
+  background-color: var(--#{$prefix}bg-surface);
+  min-height: 0;
+}
diff --git a/netbox/project-static/styles/transitional/_navigation.scss b/netbox/project-static/styles/transitional/_navigation.scss
new file mode 100644
index 000000000..fe7b8e75a
--- /dev/null
+++ b/netbox/project-static/styles/transitional/_navigation.scss
@@ -0,0 +1,38 @@
+// Navbar styling
+.navbar-vertical.navbar-expand-lg {
+  .navbar-collapse {
+    .dropdown-menu {
+      .dropdown-item {
+        a {
+          color: inherit;
+        }
+        .btn-group {
+          visibility: hidden;
+        }
+
+        // Adjust hover color & style for menu items
+        &:hover {
+          background-color: $gray-700;
+          a {
+            text-decoration: none;
+          }
+          .btn-group {
+            visibility: visible;
+          }
+        }
+
+        // Style active menu item
+        &.active {
+          background-color: $gray-700;
+          a {
+            color: white;
+          }
+          .btn-group {
+            visibility: visible;
+          }
+        }
+
+      }
+    }
+  }
+}
diff --git a/netbox/project-static/styles/transitional/_progress.scss b/netbox/project-static/styles/transitional/_progress.scss
new file mode 100644
index 000000000..08a2d3135
--- /dev/null
+++ b/netbox/project-static/styles/transitional/_progress.scss
@@ -0,0 +1,10 @@
+.progress {
+  height: 20px;
+
+  .progress-label {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    padding-left: 0.25rem;
+  }
+}
diff --git a/netbox/project-static/styles/transitional/_tables.scss b/netbox/project-static/styles/transitional/_tables.scss
new file mode 100644
index 000000000..6ac17c59c
--- /dev/null
+++ b/netbox/project-static/styles/transitional/_tables.scss
@@ -0,0 +1,46 @@
+// Object list tables
+table.object-list {
+
+  // Hide border of last row
+  tbody > tr:last-child > td {
+    border-bottom-width: 0;
+  }
+
+  // Append up/down arrow to header of column used for sorting
+  th.asc > a::after {
+    content: '\f0140';
+    font-family: 'Material Design Icons';
+  }
+  th.desc > a::after {
+    content: '\f0143';
+    font-family: 'Material Design Icons';
+  }
+
+}
+
+// Object attribute tables
+table.attr-table {
+
+  // Restyle row header
+  th {
+    color: $gray-700;
+    font-weight: normal;
+    width: min-content;
+  }
+
+  // Restyle row border
+  th, td {
+    border-bottom-style: dashed;
+  }
+
+  // Hide border of last row in table
+  tr:last-child {
+    border-bottom-style: hidden;
+  }
+
+}
+
+// Remove bottom margin from 
 elements inside table cells
+td pre {
+  margin-bottom: 0;
+}
diff --git a/netbox/project-static/styles/transitional/_tabs.scss b/netbox/project-static/styles/transitional/_tabs.scss
new file mode 100644
index 000000000..d00086f80
--- /dev/null
+++ b/netbox/project-static/styles/transitional/_tabs.scss
@@ -0,0 +1,25 @@
+// In-page content tabs
+.page-tabs {
+  // Set the border on page-tabs to ensure it expands the full page width
+  border-bottom: 1px solid $card-border-color;
+
+  .nav-tabs {
+    position: relative;
+    border: none;
+
+    .nav-link {
+      &.active,
+      &:active,
+      &:hover {
+        border-color: $card-border-color;
+        border-bottom-color: transparent;
+      }
+
+      &.active {
+        color: $headings-color;
+        background: var(--#{$prefix}bg-surface-secondary);
+        border-bottom-color: transparent;
+      }
+    }
+  }
+}
diff --git a/netbox/project-static/styles/utilities.scss b/netbox/project-static/styles/utilities.scss
deleted file mode 100644
index a5a4bf038..000000000
--- a/netbox/project-static/styles/utilities.scss
+++ /dev/null
@@ -1,50 +0,0 @@
-// Custom Utility classes.
-
-// Utility class for extra-small text.
-.text-xs {
-  font-size: $font-size-xs !important;
-  line-height: $line-height-sm !important;
-}
-
-// Add a border color that mimics the appearance of a form element.
-.border-input {
-  border: $input-border-width solid $input-border-color !important;
-}
-
-// Force no line wrapping.
-.ws-nowrap {
-  white-space: nowrap !important;
-}
-
-// Vertically align table rows/cells.
-table tr,
-table td {
-  .vertical-align {
-    vertical-align: middle;
-  }
-}
-
-// Hide element when printing.
-.noprint {
-  @media print {
-    display: none !important;
-    visibility: hidden !important;
-  }
-}
-
-// Only show element when printing, otherwise hide.
-.printonly {
-  display: none !important;
-  visibility: hidden !important;
-
-  @media print {
-    display: block !important;
-    visibility: visible !important;
-  }
-}
-
-// Hides the last child of an element
-.hide-last-child :last-child {
-  visibility: hidden;
-  opacity: 0;
-}
diff --git a/netbox/project-static/styles/variables.scss b/netbox/project-static/styles/variables.scss
deleted file mode 100644
index 8075cf5b0..000000000
--- a/netbox/project-static/styles/variables.scss
+++ /dev/null
@@ -1,55 +0,0 @@
-// NetBox CSS Variables
-
-:root {
-  // Light Mode Variables.
-  --nbx-sidebar-bg: #{$gray-200};
-  --nbx-sidebar-scroll: #{$gray-500};
-  --nbx-sidebar-link-hover-bg: #{rgba($gray-600, 0.15)};
-  --nbx-sidebar-link-active-bg: #9cc8f8;
-  --nbx-sidebar-title-color: #{$text-muted};
-  --nbx-sidebar-shadow: inset 0px -25px 20px -25px rgba(0, 0, 0, 0.25);
-  --nbx-breadcrumb-bg: #{$light};
-  --nbx-body-bg: #{$white};
-  --nbx-body-color: #{$gray-800};
-  --nbx-pre-bg: #{$gray-100};
-  --nbx-pre-border-color: #{$gray-600};
-  --nbx-change-added: #{rgba($green, 0.4)};
-  --nbx-change-removed: #{rgba($red, 0.4)};
-  --nbx-cable-node-bg: #{$gray-100};
-  --nbx-cable-node-border-color: #{$gray-200};
-  --nbx-cable-termination-bg: #{$gray-200};
-  --nbx-cable-termination-border-color: #{$gray-300};
-  --nbx-search-filter-border-left-color: #{$gray-300};
-  --nbx-color-mode-toggle-color: #{$primary};
-  --nbx-sidenav-link-color: #{$gray-800};
-  --nbx-sidenav-pin-color: #{$orange};
-  --nbx-sidenav-parent-color: #{$gray-800};
-  --nbx-sidenav-group-color: #{$gray-800};
-
-  &[data-netbox-color-mode='dark'] {
-    // Dark Mode Variables.
-    --nbx-sidebar-bg: #{$gray-900};
-    --nbx-sidebar-scroll: #{$gray-700};
-    --nbx-sidebar-link-active-bg: #{rgba($blue-300, 0.25)};
-    --nbx-sidebar-link-hover-bg: #{rgba($gray-500, 0.15)};
-    --nbx-sidebar-title-color: #{$gray-600};
-    --nbx-sidebar-shadow: inset 0px -25px 20px -25px rgba(255, 255, 255, 0.05);
-    --nbx-breadcrumb-bg: #{$gray-800};
-    --nbx-body-bg: #{$darker};
-    --nbx-body-color: #{$gray-100};
-    --nbx-pre-bg: #{$gray-700};
-    --nbx-pre-border-color: #{$gray-600};
-    --nbx-change-added: #{rgba($green-300, 0.4)};
-    --nbx-change-removed: #{rgba($red-300, 0.4)};
-    --nbx-cable-node-bg: #{$gray-700};
-    --nbx-cable-node-border-color: #{$gray-600};
-    --nbx-cable-termination-bg: #{$gray-800};
-    --nbx-cable-termination-border-color: #{$gray-700};
-    --nbx-search-filter-border-left-color: #{$gray-600};
-    --nbx-color-mode-toggle-color: #{$yellow-300};
-    --nbx-sidenav-link-color: #{$gray-200};
-    --nbx-sidenav-pin-color: #{$yellow};
-    --nbx-sidenav-parent-color: #{$gray-200};
-    --nbx-sidenav-group-color: #{$gray-600};
-  }
-}
diff --git a/netbox/project-static/yarn.lock b/netbox/project-static/yarn.lock
index 583b434f8..14c62ad03 100644
--- a/netbox/project-static/yarn.lock
+++ b/netbox/project-static/yarn.lock
@@ -2,14 +2,9 @@
 # yarn lockfile v1
 
 
-"@esbuild/linux-loong64@0.14.54":
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
-  integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
-
 "@eslint/eslintrc@^1.3.2":
   version "1.3.2"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356"
+  resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz"
   integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==
   dependencies:
     ajv "^6.12.4"
@@ -24,7 +19,7 @@
 
 "@graphiql/toolkit@^0.4.4":
   version "0.4.5"
-  resolved "https://registry.yarnpkg.com/@graphiql/toolkit/-/toolkit-0.4.5.tgz#9fb5020444712562eae9581724695c235f610836"
+  resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.4.5.tgz"
   integrity sha512-QXuuMSSK/0GfBS7tltrGZdyhIvm6oe9TK4VW9pfa8dALYttpzyJ64Q4Sx9I1Ng++yOMJWziM/ksa043zkNHsjQ==
   dependencies:
     "@n1ru4l/push-pull-async-iterable-iterator" "^3.1.0"
@@ -32,7 +27,7 @@
 
 "@humanwhocodes/config-array@^0.10.5":
   version "0.10.7"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.7.tgz#6d53769fd0c222767e6452e8ebda825c22e9f0dc"
+  resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz"
   integrity sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==
   dependencies:
     "@humanwhocodes/object-schema" "^1.2.1"
@@ -41,50 +36,45 @@
 
 "@humanwhocodes/gitignore-to-minimatch@^1.0.2":
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d"
+  resolved "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz"
   integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==
 
 "@humanwhocodes/module-importer@^1.0.1":
   version "1.0.1"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
+  resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz"
   integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
 
 "@humanwhocodes/object-schema@^1.2.1":
   version "1.2.1"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
+  resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz"
   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
-"@juggle/resize-observer@^3.3.1":
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0"
-  integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==
-
 "@mdi/font@^7.0.96":
   version "7.0.96"
-  resolved "https://registry.yarnpkg.com/@mdi/font/-/font-7.0.96.tgz#9853c222623072f5575b4039c8c195ea929b61fc"
+  resolved "https://registry.npmjs.org/@mdi/font/-/font-7.0.96.tgz"
   integrity sha512-rzlxTfR64hqY8yiBzDjmANfcd8rv+T5C0Yedv/TWk2QyAQYdc66e0kaN1ipmnYU3RukHRTRcBARHzzm+tIhL7w==
 
 "@n1ru4l/push-pull-async-iterable-iterator@^3.1.0":
   version "3.2.0"
-  resolved "https://registry.yarnpkg.com/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz#c15791112db68dd9315d329d652b7e797f737655"
+  resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz"
   integrity sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==
 
 "@nodelib/fs.scandir@2.1.5":
   version "2.1.5"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+  resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
   integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
   dependencies:
     "@nodelib/fs.stat" "2.0.5"
     run-parallel "^1.1.9"
 
-"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
   version "2.0.5"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+  resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
   integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
 
 "@nodelib/fs.walk@^1.2.3":
   version "1.2.8"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+  resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
   integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
   dependencies:
     "@nodelib/fs.scandir" "2.1.5"
@@ -92,7 +82,7 @@
 
 "@pkgr/utils@^2.3.1":
   version "2.3.1"
-  resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.3.1.tgz#0a9b06ffddee364d6642b3cd562ca76f55b34a03"
+  resolved "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz"
   integrity sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==
   dependencies:
     cross-spawn "^7.0.3"
@@ -102,55 +92,55 @@
     tiny-glob "^0.2.9"
     tslib "^2.4.0"
 
-"@popperjs/core@^2.11.6", "@popperjs/core@^2.9.2":
+"@popperjs/core@^2.11.8":
+  version "2.11.8"
+  resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
+  integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
+
+"@popperjs/core@^2.9.2":
   version "2.11.6"
-  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
+  resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz"
   integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
 
-"@types/bootstrap@^5.0.17":
-  version "5.2.5"
-  resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.5.tgz#0bb5dea7720611b2bb7ba16bd8a64fafd86fb658"
-  integrity sha512-VnalUJ3E/oaV3DYrauEc/sSPpaEPxTV09twSEzY4KFRvyuGlrZUSqG95XZ6ReAi0YMZIs7rXxdngDK2X1YONQA==
+"@tabler/core@1.0.0-beta20":
+  version "1.0.0-beta20"
+  resolved "https://registry.npmjs.org/@tabler/core/-/core-1.0.0-beta20.tgz"
+  integrity sha512-OzKpur+Ug7e+HMbNJrMcSuWZGUsJTvu7HYboBNRE8qyo1RKIWqvwL5YewKBJ+odW5pDOqBPzbsS4je3EBQQxHw==
+  dependencies:
+    "@popperjs/core" "^2.11.8"
+    "@tabler/icons" "^2.32.0"
+    bootstrap "5.3.1"
+
+"@tabler/icons@^2.32.0":
+  version "2.44.0"
+  resolved "https://registry.npmjs.org/@tabler/icons/-/icons-2.44.0.tgz"
+  integrity sha512-WPPtihDcAwEm1QZM9MXQw6+r/R2/qx7KMU1eegsi9DsqBLAb0W2kbt6e/syvd6j9c+6XNpRVBW1ziGqSWQAWOg==
+
+"@types/bootstrap@5.2.10":
+  version "5.2.10"
+  resolved "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz"
+  integrity sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==
   dependencies:
     "@popperjs/core" "^2.9.2"
 
 "@types/cookie@^0.5.1":
   version "0.5.1"
-  resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.5.1.tgz#b29aa1f91a59f35e29ff8f7cb24faf1a3a750554"
+  resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz"
   integrity sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==
 
-"@types/jquery@*":
-  version "3.5.6"
-  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.6.tgz#97ac8e36dccd8ad8ed3f3f3b48933614d9fd8cf0"
-  integrity sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==
-  dependencies:
-    "@types/sizzle" "*"
-
 "@types/json-schema@^7.0.9":
   version "7.0.11"
-  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
+  resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz"
   integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
 
 "@types/json5@^0.0.29":
   version "0.0.29"
-  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
+  resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
   integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
 
-"@types/masonry-layout@^4.2.5":
-  version "4.2.5"
-  resolved "https://registry.yarnpkg.com/@types/masonry-layout/-/masonry-layout-4.2.5.tgz#1d6180ec07ad21df7f85d20ac6e99b061854d00f"
-  integrity sha512-/DSIDMDsXlS+7JfzywA2zm9x+veO/z1QDcjKLS4OhDZH7sFdreKJbNFKb7jYA3NrY3bRvFGCamp5+DeIArLzow==
-  dependencies:
-    "@types/jquery" "*"
-
-"@types/sizzle@*":
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
-  integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
-
 "@typescript-eslint/eslint-plugin@^5.39.0":
   version "5.39.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.39.0.tgz#778b2d9e7f293502c7feeea6c74dca8eb3e67511"
+  resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.39.0.tgz"
   integrity sha512-xVfKOkBm5iWMNGKQ2fwX5GVgBuHmZBO1tCRwXmY5oAIsPscfwm2UADDuNB8ZVYCtpQvJK4xpjrK7jEhcJ0zY9A==
   dependencies:
     "@typescript-eslint/scope-manager" "5.39.0"
@@ -162,9 +152,9 @@
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/parser@^5.39.0":
+"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.39.0":
   version "5.39.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.39.0.tgz#93fa0bc980a3a501e081824f6097f7ca30aaa22b"
+  resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.39.0.tgz"
   integrity sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA==
   dependencies:
     "@typescript-eslint/scope-manager" "5.39.0"
@@ -174,7 +164,7 @@
 
 "@typescript-eslint/scope-manager@5.39.0":
   version "5.39.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz#873e1465afa3d6c78d8ed2da68aed266a08008d0"
+  resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz"
   integrity sha512-/I13vAqmG3dyqMVSZPjsbuNQlYS082Y7OMkwhCfLXYsmlI0ca4nkL7wJ/4gjX70LD4P8Hnw1JywUVVAwepURBw==
   dependencies:
     "@typescript-eslint/types" "5.39.0"
@@ -182,7 +172,7 @@
 
 "@typescript-eslint/type-utils@5.39.0":
   version "5.39.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.39.0.tgz#0a8c00f95dce4335832ad2dc6bc431c14e32a0a6"
+  resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.39.0.tgz"
   integrity sha512-KJHJkOothljQWzR3t/GunL0TPKY+fGJtnpl+pX+sJ0YiKTz3q2Zr87SGTmFqsCMFrLt5E0+o+S6eQY0FAXj9uA==
   dependencies:
     "@typescript-eslint/typescript-estree" "5.39.0"
@@ -192,12 +182,12 @@
 
 "@typescript-eslint/types@5.39.0":
   version "5.39.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.39.0.tgz#f4e9f207ebb4579fd854b25c0bf64433bb5ed78d"
+  resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.39.0.tgz"
   integrity sha512-gQMZrnfEBFXK38hYqt8Lkwt8f4U6yq+2H5VDSgP/qiTzC8Nw8JO3OuSUOQ2qW37S/dlwdkHDntkZM6SQhKyPhw==
 
 "@typescript-eslint/typescript-estree@5.39.0":
   version "5.39.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz#c0316aa04a1a1f4f7f9498e3c13ef1d3dc4cf88b"
+  resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz"
   integrity sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA==
   dependencies:
     "@typescript-eslint/types" "5.39.0"
@@ -210,7 +200,7 @@
 
 "@typescript-eslint/utils@5.39.0":
   version "5.39.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.39.0.tgz#b7063cca1dcf08d1d21b0d91db491161ad0be110"
+  resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.39.0.tgz"
   integrity sha512-+DnY5jkpOpgj+EBtYPyHRjXampJfC0yUZZzfzLuUWVZvCuKqSdJVC8UhdWipIw7VKNTfwfAPiOWzYkAwuIhiAg==
   dependencies:
     "@types/json-schema" "^7.0.9"
@@ -222,7 +212,7 @@
 
 "@typescript-eslint/visitor-keys@5.39.0":
   version "5.39.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz#8f41f7d241b47257b081ddba5d3ce80deaae61e2"
+  resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz"
   integrity sha512-yyE3RPwOG+XJBLrhvsxAidUgybJVQ/hG8BhiJo0k8JSAYfk/CshVcxf0HwP4Jt7WZZ6vLmxdo1p6EyN3tzFTkg==
   dependencies:
     "@typescript-eslint/types" "5.39.0"
@@ -230,17 +220,17 @@
 
 acorn-jsx@^5.3.2:
   version "5.3.2"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+  resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
-acorn@^8.8.0:
+"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.8.0:
   version "8.8.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
+  resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz"
   integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
 
 ajv@^6.10.0, ajv@^6.12.4:
   version "6.12.6"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
   dependencies:
     fast-deep-equal "^3.1.1"
@@ -250,19 +240,19 @@ ajv@^6.10.0, ajv@^6.12.4:
 
 ansi-regex@^5.0.1:
   version "5.0.1"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz"
   integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
 
 ansi-styles@^4.1.0:
   version "4.3.0"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
   integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
   dependencies:
     color-convert "^2.0.1"
 
 anymatch@~3.1.2:
   version "3.1.2"
-  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz"
   integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
   dependencies:
     normalize-path "^3.0.0"
@@ -270,12 +260,12 @@ anymatch@~3.1.2:
 
 argparse@^2.0.1:
   version "2.0.1"
-  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
   integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
 
 array-includes@^3.1.4:
   version "3.1.5"
-  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb"
+  resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz"
   integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==
   dependencies:
     call-bind "^1.0.2"
@@ -286,12 +276,12 @@ array-includes@^3.1.4:
 
 array-union@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+  resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz"
   integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
 
 array.prototype.flat@^1.2.5:
   version "1.3.0"
-  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b"
+  resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz"
   integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==
   dependencies:
     call-bind "^1.0.2"
@@ -301,32 +291,32 @@ array.prototype.flat@^1.2.5:
 
 async-limiter@~1.0.0:
   version "1.0.1"
-  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+  resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz"
   integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
 
 backo2@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+  resolved "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz"
   integrity sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==
 
 balanced-match@^1.0.0:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
 binary-extensions@^2.0.0:
   version "2.2.0"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
   integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
 
-bootstrap@~5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.0.2.tgz#aff23d5e0e03c31255ad437530ee6556e78e728e"
-  integrity sha512-1Ge963tyEQWJJ+8qtXFU6wgmAVj9gweEjibUdbmcCEYsn38tVwRk8107rk2vzt6cfQcRr3SlZ8aQBqaD8aqf+Q==
+bootstrap@5.3.1:
+  version "5.3.1"
+  resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz"
+  integrity sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==
 
 brace-expansion@^1.1.7:
   version "1.1.11"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
   integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
   dependencies:
     balanced-match "^1.0.0"
@@ -334,14 +324,14 @@ brace-expansion@^1.1.7:
 
 braces@^3.0.1, braces@~3.0.2:
   version "3.0.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz"
   integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
   dependencies:
     fill-range "^7.0.1"
 
 call-bind@^1.0.0, call-bind@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+  resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
   integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
   dependencies:
     function-bind "^1.1.1"
@@ -349,17 +339,12 @@ call-bind@^1.0.0, call-bind@^1.0.2:
 
 callsites@^3.0.0:
   version "3.1.0"
-  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
-can-use-dom@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a"
-  integrity sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo=
-
 chalk@^4.0.0:
   version "4.1.2"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
   integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
   dependencies:
     ansi-styles "^4.1.0"
@@ -367,7 +352,7 @@ chalk@^4.0.0:
 
 "chokidar@>=3.0.0 <4.0.0":
   version "3.5.2"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
+  resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz"
   integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
   dependencies:
     anymatch "~3.1.2"
@@ -382,7 +367,7 @@ chalk@^4.0.0:
 
 clipboard@^2.0.11:
   version "2.0.11"
-  resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5"
+  resolved "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz"
   integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==
   dependencies:
     good-listener "^1.2.2"
@@ -391,53 +376,48 @@ clipboard@^2.0.11:
 
 codemirror-graphql@^1.3.0:
   version "1.3.2"
-  resolved "https://registry.yarnpkg.com/codemirror-graphql/-/codemirror-graphql-1.3.2.tgz#e9d1d18b4a160f0016a28465805284636ee42d2a"
+  resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-1.3.2.tgz"
   integrity sha512-glwFsEVlH5TvxjSKGymZ1sNy37f3Mes58CB4fXOd0zy9+JzDL08Wti1b5ycy4vFZYghMDK1/Or/zRSjMAGtC2w==
   dependencies:
     graphql-language-service "^5.0.6"
 
 codemirror@^5.65.3:
   version "5.65.14"
-  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.14.tgz#e75fbc7247453f1baa71463c33b52adba7e41b2a"
+  resolved "https://registry.npmjs.org/codemirror/-/codemirror-5.65.14.tgz"
   integrity sha512-VSNugIBDGt0OU9gDjeVr6fNkoFQznrWEUdAApMlXQNbfE8gGO19776D6MwSqF/V/w/sDwonsQ0z7KmmI9guScg==
 
 color-convert@^2.0.1:
   version "2.0.1"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
   integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
   dependencies:
     color-name "~1.1.4"
 
 color-name@~1.1.4:
   version "1.1.4"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
 color2k@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.yarnpkg.com/color2k/-/color2k-2.0.0.tgz#86992c82e248c29f524023ed0822bc152c4fa670"
+  resolved "https://registry.npmjs.org/color2k/-/color2k-2.0.0.tgz"
   integrity sha512-DWX9eXOC4fbJNiuvdH4QSHvvfLWyFo9TuFp7V9OzdsbPAdrWAuYc8qvFP2bIQ/LKh4LrAVnJ6vhiQYPvAHdtTg==
 
 concat-map@0.0.1:
   version "0.0.1"
-  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
   integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
 
 copy-to-clipboard@^3.2.0:
   version "3.3.1"
-  resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
+  resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz"
   integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
   dependencies:
     toggle-selection "^1.0.6"
 
-core-js@^3.0.1:
-  version "3.16.4"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.16.4.tgz#0fb1029a554fc2688c0963d7c900e188188a78e0"
-  integrity sha512-Tq4GVE6XCjE+hcyW6hPy0ofN3hwtLudz5ZRdrlCnsnD/xkm/PWQRudzYHiKgZKUcefV6Q57fhDHjZHJP5dpfSg==
-
 cross-spawn@^7.0.2, cross-spawn@^7.0.3:
   version "7.0.3"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
   dependencies:
     path-key "^3.1.0"
@@ -446,62 +426,62 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
 
 dayjs@^1.11.5:
   version "1.11.5"
-  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93"
+  resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz"
   integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==
 
 debug@^2.6.9:
   version "2.6.9"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
   integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
   dependencies:
     ms "2.0.0"
 
 debug@^3.2.7:
   version "3.2.7"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
   integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
   dependencies:
     ms "^2.1.1"
 
 debug@^4.1.1:
   version "4.3.2"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz"
   integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
   dependencies:
     ms "2.1.2"
 
 debug@^4.3.2, debug@^4.3.4:
   version "4.3.4"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
   integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
   dependencies:
     ms "2.1.2"
 
 decode-uri-component@^0.2.0:
   version "0.2.0"
-  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+  resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz"
   integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
 
 deep-is@^0.1.3:
   version "0.1.3"
-  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+  resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
 define-lazy-prop@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
+  resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz"
   integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
 
 define-properties@^1.1.3:
   version "1.1.3"
-  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+  resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz"
   integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
   dependencies:
     object-keys "^1.0.12"
 
 define-properties@^1.1.4:
   version "1.1.4"
-  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
+  resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz"
   integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==
   dependencies:
     has-property-descriptors "^1.0.0"
@@ -509,33 +489,33 @@ define-properties@^1.1.4:
 
 delegate@^3.1.2:
   version "3.2.0"
-  resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
+  resolved "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz"
   integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
 
 dir-glob@^3.0.1:
   version "3.0.1"
-  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+  resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
   integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
   dependencies:
     path-type "^4.0.0"
 
 doctrine@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+  resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz"
   integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
   dependencies:
     esutils "^2.0.2"
 
 doctrine@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+  resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz"
   integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
   dependencies:
     esutils "^2.0.2"
 
 enhanced-resolve@^5.10.0:
   version "5.10.0"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
+  resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz"
   integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==
   dependencies:
     graceful-fs "^4.2.4"
@@ -543,17 +523,17 @@ enhanced-resolve@^5.10.0:
 
 entities@^2.0.0:
   version "2.2.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz"
   integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
 
 entities@~2.1.0:
   version "2.1.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
+  resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz"
   integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
 
 es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5:
   version "1.20.3"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.3.tgz#90b143ff7aedc8b3d189bcfac7f1e3e3f81e9da1"
+  resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz"
   integrity sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==
   dependencies:
     call-bind "^1.0.2"
@@ -583,217 +563,42 @@ es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19
 
 es-shim-unscopables@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241"
+  resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz"
   integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==
   dependencies:
     has "^1.0.3"
 
 es-to-primitive@^1.2.1:
   version "1.2.1"
-  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz"
   integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
   dependencies:
     is-callable "^1.1.4"
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
-esbuild-android-64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
-  integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==
-
-esbuild-android-arm64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44"
-  integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==
-
-esbuild-android-arm64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771"
-  integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==
-
-esbuild-darwin-64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72"
-  integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==
-
-esbuild-darwin-64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25"
-  integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==
-
-esbuild-darwin-arm64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a"
-  integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==
-
-esbuild-darwin-arm64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73"
-  integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==
-
-esbuild-freebsd-64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85"
-  integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==
-
-esbuild-freebsd-64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d"
-  integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==
-
-esbuild-freebsd-arm64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52"
-  integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==
-
-esbuild-freebsd-arm64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48"
-  integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==
-
-esbuild-linux-32@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69"
-  integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==
-
-esbuild-linux-32@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5"
-  integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==
-
 esbuild-linux-64@0.13.15:
   version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3"
+  resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz"
   integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==
 
 esbuild-linux-64@0.14.54:
   version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652"
+  resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz"
   integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
 
-esbuild-linux-arm64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1"
-  integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==
-
-esbuild-linux-arm64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b"
-  integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==
-
-esbuild-linux-arm@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe"
-  integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==
-
-esbuild-linux-arm@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59"
-  integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==
-
-esbuild-linux-mips64le@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7"
-  integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==
-
-esbuild-linux-mips64le@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34"
-  integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==
-
-esbuild-linux-ppc64le@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2"
-  integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==
-
-esbuild-linux-ppc64le@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e"
-  integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==
-
-esbuild-linux-riscv64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8"
-  integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==
-
-esbuild-linux-s390x@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6"
-  integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==
-
-esbuild-netbsd-64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038"
-  integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==
-
-esbuild-netbsd-64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81"
-  integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==
-
-esbuild-openbsd-64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7"
-  integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==
-
-esbuild-openbsd-64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b"
-  integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==
-
 esbuild-sass-plugin@^2.3.3:
   version "2.3.3"
-  resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-2.3.3.tgz#0b35eb6ba82caca8f85eed4163d2f2df2f7f7d1c"
+  resolved "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.3.3.tgz"
   integrity sha512-EegGnUIsP5Y7FbwcGBD524F+cJaIAQU2LSOX9QtjgpqEmwnmfEh5f/aPJ1df5GxD3NgHQJspeRCV7spDHE3N6Q==
   dependencies:
     esbuild "^0.14.13"
     resolve "^1.22.1"
     sass "^1.49.0"
 
-esbuild-sunos-64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4"
-  integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==
-
-esbuild-sunos-64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
-  integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==
-
-esbuild-windows-32@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7"
-  integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==
-
-esbuild-windows-32@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31"
-  integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==
-
-esbuild-windows-64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294"
-  integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==
-
-esbuild-windows-64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4"
-  integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==
-
-esbuild-windows-arm64@0.13.15:
-  version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3"
-  integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==
-
-esbuild-windows-arm64@0.14.54:
-  version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982"
-  integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==
-
 esbuild@^0.13.15:
   version "0.13.15"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf"
+  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz"
   integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==
   optionalDependencies:
     esbuild-android-arm64 "0.13.15"
@@ -816,7 +621,7 @@ esbuild@^0.13.15:
 
 esbuild@^0.14.13:
   version "0.14.54"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
+  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz"
   integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
   optionalDependencies:
     "@esbuild/linux-loong64" "0.14.54"
@@ -843,22 +648,22 @@ esbuild@^0.14.13:
 
 escape-html@^1.0.3:
   version "1.0.3"
-  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz"
   integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
 
 escape-string-regexp@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
 
 eslint-config-prettier@^8.5.0:
   version "8.5.0"
-  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1"
+  resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz"
   integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==
 
 eslint-import-resolver-node@^0.3.6:
   version "0.3.6"
-  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"
+  resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz"
   integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==
   dependencies:
     debug "^3.2.7"
@@ -866,7 +671,7 @@ eslint-import-resolver-node@^0.3.6:
 
 eslint-import-resolver-typescript@^3.5.1:
   version "3.5.1"
-  resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.1.tgz#c72634da072eebd04fe73007fa58a62c333c8147"
+  resolved "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.1.tgz"
   integrity sha512-U7LUjNJPYjNsHvAUAkt/RU3fcTSpbllA0//35B4eLYTX74frmOepbt7F7J3D1IGtj9k21buOpaqtDd4ZlS/BYQ==
   dependencies:
     debug "^4.3.4"
@@ -879,14 +684,14 @@ eslint-import-resolver-typescript@^3.5.1:
 
 eslint-module-utils@^2.7.3:
   version "2.7.4"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974"
+  resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz"
   integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==
   dependencies:
     debug "^3.2.7"
 
-eslint-plugin-import@^2.26.0:
+eslint-plugin-import@*, eslint-plugin-import@^2.26.0:
   version "2.26.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b"
+  resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz"
   integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==
   dependencies:
     array-includes "^3.1.4"
@@ -905,14 +710,14 @@ eslint-plugin-import@^2.26.0:
 
 eslint-plugin-prettier@^4.2.1:
   version "4.2.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
+  resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz"
   integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
   dependencies:
     prettier-linter-helpers "^1.0.0"
 
 eslint-scope@^5.1.1:
   version "5.1.1"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"
   integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
   dependencies:
     esrecurse "^4.3.0"
@@ -920,7 +725,7 @@ eslint-scope@^5.1.1:
 
 eslint-scope@^7.1.1:
   version "7.1.1"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642"
+  resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz"
   integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==
   dependencies:
     esrecurse "^4.3.0"
@@ -928,24 +733,24 @@ eslint-scope@^7.1.1:
 
 eslint-utils@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
+  resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz"
   integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
   dependencies:
     eslint-visitor-keys "^2.0.0"
 
 eslint-visitor-keys@^2.0.0:
   version "2.1.0"
-  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
+  resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz"
   integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
 
 eslint-visitor-keys@^3.3.0:
   version "3.3.0"
-  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
+  resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz"
   integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
 
-eslint@^8.24.0:
+eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", eslint@^8.24.0, eslint@>=5, eslint@>=7.0.0, eslint@>=7.28.0:
   version "8.24.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.24.0.tgz#489516c927a5da11b3979dbfb2679394523383c8"
+  resolved "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz"
   integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==
   dependencies:
     "@eslint/eslintrc" "^1.3.2"
@@ -990,7 +795,7 @@ eslint@^8.24.0:
 
 espree@^9.4.0:
   version "9.4.0"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a"
+  resolved "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz"
   integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==
   dependencies:
     acorn "^8.8.0"
@@ -999,51 +804,51 @@ espree@^9.4.0:
 
 esquery@^1.4.0:
   version "1.4.0"
-  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
+  resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz"
   integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
   dependencies:
     estraverse "^5.1.0"
 
 esrecurse@^4.3.0:
   version "4.3.0"
-  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz"
   integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
   dependencies:
     estraverse "^5.2.0"
 
 estraverse@^4.1.1:
   version "4.3.0"
-  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
   integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
 
 estraverse@^5.1.0, estraverse@^5.2.0:
   version "5.2.0"
-  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
+  resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz"
   integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
 
 esutils@^2.0.2:
   version "2.0.3"
-  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
 eventemitter3@^3.1.0:
   version "3.1.2"
-  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
+  resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz"
   integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
 
 fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
 fast-diff@^1.1.2:
   version "1.2.0"
-  resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
+  resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz"
   integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
 
 fast-glob@^3.2.11, fast-glob@^3.2.9:
   version "3.2.12"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
+  resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz"
   integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
   dependencies:
     "@nodelib/fs.stat" "^2.0.2"
@@ -1054,43 +859,43 @@ fast-glob@^3.2.11, fast-glob@^3.2.9:
 
 fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
-  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
 fast-levenshtein@^2.0.6:
   version "2.0.6"
-  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
 
 fastq@^1.6.0:
   version "1.12.0"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.12.0.tgz#ed7b6ab5d62393fb2cc591c853652a5c318bf794"
+  resolved "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz"
   integrity sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==
   dependencies:
     reusify "^1.0.4"
 
 file-entry-cache@^6.0.1:
   version "6.0.1"
-  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+  resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
   integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
   dependencies:
     flat-cache "^3.0.4"
 
 fill-range@^7.0.1:
   version "7.0.1"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
   integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
   dependencies:
     to-regex-range "^5.0.1"
 
 filter-obj@^1.1.0:
   version "1.1.0"
-  resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b"
+  resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz"
   integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs=
 
 find-up@^5.0.0:
   version "5.0.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+  resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz"
   integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
   dependencies:
     locate-path "^6.0.0"
@@ -1098,40 +903,35 @@ find-up@^5.0.0:
 
 flat-cache@^3.0.4:
   version "3.0.4"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
+  resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz"
   integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
   dependencies:
     flatted "^3.1.0"
     rimraf "^3.0.2"
 
-flatpickr@4.6.13:
+flatpickr@^4.6.13, flatpickr@4.6.13:
   version "4.6.13"
-  resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.13.tgz#8a029548187fd6e0d670908471e43abe9ad18d94"
+  resolved "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz"
   integrity sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==
 
 flatted@^3.1.0:
   version "3.2.2"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
+  resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz"
   integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
 
 fs.realpath@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
-fsevents@~2.3.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
-  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-
 function-bind@^1.1.1:
   version "1.1.1"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
 function.prototype.name@^1.1.5:
   version "1.1.5"
-  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
+  resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz"
   integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
   dependencies:
     call-bind "^1.0.2"
@@ -1141,12 +941,12 @@ function.prototype.name@^1.1.5:
 
 functions-have-names@^1.2.2:
   version "1.2.3"
-  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
+  resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz"
   integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
 
 get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
   version "1.1.1"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
+  resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz"
   integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
   dependencies:
     function-bind "^1.1.1"
@@ -1155,7 +955,7 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
 
 get-intrinsic@^1.1.3:
   version "1.1.3"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385"
+  resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz"
   integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==
   dependencies:
     function-bind "^1.1.1"
@@ -1164,7 +964,7 @@ get-intrinsic@^1.1.3:
 
 get-symbol-description@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+  resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz"
   integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
   dependencies:
     call-bind "^1.0.2"
@@ -1172,26 +972,26 @@ get-symbol-description@^1.0.0:
 
 get-tsconfig@^4.2.0:
   version "4.2.0"
-  resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.2.0.tgz#ff368dd7104dab47bf923404eb93838245c66543"
+  resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.2.0.tgz"
   integrity sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==
 
 glob-parent@^5.1.2, glob-parent@~5.1.2:
   version "5.1.2"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
   dependencies:
     is-glob "^4.0.1"
 
 glob-parent@^6.0.1:
   version "6.0.2"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+  resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz"
   integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
   dependencies:
     is-glob "^4.0.3"
 
 glob@^7.1.3:
   version "7.1.7"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
+  resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz"
   integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
   dependencies:
     fs.realpath "^1.0.0"
@@ -1203,19 +1003,19 @@ glob@^7.1.3:
 
 globals@^13.15.0:
   version "13.17.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4"
+  resolved "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz"
   integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==
   dependencies:
     type-fest "^0.20.2"
 
 globalyzer@0.1.0:
   version "0.1.0"
-  resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465"
+  resolved "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz"
   integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==
 
 globby@^11.1.0:
   version "11.1.0"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
+  resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz"
   integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
   dependencies:
     array-union "^2.1.0"
@@ -1227,7 +1027,7 @@ globby@^11.1.0:
 
 globby@^13.1.2:
   version "13.1.2"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515"
+  resolved "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz"
   integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==
   dependencies:
     dir-glob "^3.0.1"
@@ -1238,29 +1038,29 @@ globby@^13.1.2:
 
 globrex@^0.1.2:
   version "0.1.2"
-  resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
+  resolved "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz"
   integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
 
 good-listener@^1.2.2:
   version "1.2.2"
-  resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
+  resolved "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz"
   integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=
   dependencies:
     delegate "^3.1.2"
 
 graceful-fs@^4.2.4:
   version "4.2.10"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
+  resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz"
   integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
 
 grapheme-splitter@^1.0.4:
   version "1.0.4"
-  resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
+  resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz"
   integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
 
 graphiql@1.8.9:
   version "1.8.9"
-  resolved "https://registry.yarnpkg.com/graphiql/-/graphiql-1.8.9.tgz#96063ace9379699e873da078bf0f2ae7062f370e"
+  resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.8.9.tgz"
   integrity sha512-X+olqol3VfOrFrsAfYzgNbvHoEE0lDUtg52+f3yLeNY4Ens+rAH4RSdH4wVtoRdh3CafmhVsfLQvn04hHkgftQ==
   dependencies:
     "@graphiql/toolkit" "^0.4.4"
@@ -1275,7 +1075,7 @@ graphiql@1.8.9:
 
 graphql-language-service@^5.0.4:
   version "5.1.7"
-  resolved "https://registry.yarnpkg.com/graphql-language-service/-/graphql-language-service-5.1.7.tgz#2b35df573de265eb2623843721702f07ff9ee130"
+  resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.1.7.tgz"
   integrity sha512-xkawYMJeoNYGhT+SpSH3c2qf6HpGHQ/duDmrseVHBpVCrXAiGnliXGSCC4jyMGgZQ05GytsZ12p0nUo7s6lSSw==
   dependencies:
     nullthrows "^1.0.0"
@@ -1283,91 +1083,96 @@ graphql-language-service@^5.0.4:
 
 graphql-language-service@^5.0.6:
   version "5.0.6"
-  resolved "https://registry.yarnpkg.com/graphql-language-service/-/graphql-language-service-5.0.6.tgz#7fd1e6479e5c3074b070c760fa961d9ad1ed7c72"
+  resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.0.6.tgz"
   integrity sha512-FjE23aTy45Lr5metxCv3ZgSKEZOzN7ERR+OFC1isV5mHxI0Ob8XxayLTYjQKrs8b3kOpvgTYmSmu6AyXOzYslg==
   dependencies:
     nullthrows "^1.0.0"
     vscode-languageserver-types "^3.15.1"
 
-"graphql@>= v14.5.0 <= 15.5.0":
+"graphql@^15.5.0 || ^16.0.0", "graphql@>= v14.5.0 <= 15.5.0", graphql@>=0.10.0:
   version "15.5.0"
-  resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5"
+  resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz"
   integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==
 
 gridstack@^7.2.3:
   version "7.2.3"
-  resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-7.2.3.tgz#bc04d3588eb5f2b7edd910e31fdac5bea8069ff2"
+  resolved "https://registry.npmjs.org/gridstack/-/gridstack-7.2.3.tgz"
   integrity sha512-1s4Fx+Hr4nKl064q/ygrd41XiZaC2gG6R+yz5nbOibP9vODJ6mOtjIM5x8qKN12FknakaMpVBnCa1T6V7H15hQ==
 
 has-bigints@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
+  resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz"
   integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
 
 has-bigints@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
+  resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz"
   integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
 
 has-flag@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
 has-property-descriptors@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
+  resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz"
   integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
   dependencies:
     get-intrinsic "^1.1.1"
 
-has-symbols@^1.0.1, has-symbols@^1.0.2:
+has-symbols@^1.0.1:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
+  resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz"
+  integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
+
+has-symbols@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz"
   integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
 
 has-symbols@^1.0.3:
   version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+  resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz"
   integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
 
 has-tostringtag@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
+  resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz"
   integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
   dependencies:
     has-symbols "^1.0.2"
 
 has@^1.0.3:
   version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
   integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
   dependencies:
     function-bind "^1.1.1"
 
 html-entities@^2.3.3:
   version "2.3.3"
-  resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
+  resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz"
   integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
 
 htmx.org@^1.8.0:
   version "1.8.0"
-  resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-1.8.0.tgz#f3a2f681f3e2b6357b5a29bba24a2572a8e48fd3"
+  resolved "https://registry.npmjs.org/htmx.org/-/htmx.org-1.8.0.tgz"
   integrity sha512-xdR2PeSmhftFhUKn/5DYDFRVF8DagJR9d7y3AK+gQzoAQ+08r+0shaCTo1HdXKGKhRfX/uL3rqj4ZwCBNf8tLw==
 
 ignore@^5.2.0:
   version "5.2.0"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
+  resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz"
   integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
 
 immutable@^4.0.0:
   version "4.1.0"
-  resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
+  resolved "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz"
   integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
 
 import-fresh@^3.0.0, import-fresh@^3.2.1:
   version "3.3.0"
-  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"
   integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
   dependencies:
     parent-module "^1.0.0"
@@ -1375,12 +1180,12 @@ import-fresh@^3.0.0, import-fresh@^3.2.1:
 
 imurmurhash@^0.1.4:
   version "0.1.4"
-  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+  resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
   integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
 
 inflight@^1.0.4:
   version "1.0.6"
-  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
   integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
   dependencies:
     once "^1.3.0"
@@ -1388,12 +1193,12 @@ inflight@^1.0.4:
 
 inherits@2:
   version "2.0.4"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
 internal-slot@^1.0.3:
   version "1.0.3"
-  resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
+  resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz"
   integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==
   dependencies:
     get-intrinsic "^1.1.0"
@@ -1402,21 +1207,21 @@ internal-slot@^1.0.3:
 
 is-bigint@^1.0.1:
   version "1.0.4"
-  resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
+  resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz"
   integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
   dependencies:
     has-bigints "^1.0.1"
 
 is-binary-path@~2.1.0:
   version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
   integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
   dependencies:
     binary-extensions "^2.0.0"
 
 is-boolean-object@^1.1.0:
   version "1.1.2"
-  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
+  resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz"
   integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
   dependencies:
     call-bind "^1.0.2"
@@ -1424,91 +1229,105 @@ is-boolean-object@^1.1.0:
 
 is-callable@^1.1.4:
   version "1.2.4"
-  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
+  resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz"
   integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
 
 is-callable@^1.2.6:
   version "1.2.7"
-  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
+  resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz"
   integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
 
 is-core-module@^2.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0:
   version "2.10.0"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed"
+  resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz"
   integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==
   dependencies:
     has "^1.0.3"
 
 is-core-module@^2.2.0:
   version "2.6.0"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19"
+  resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz"
   integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==
   dependencies:
     has "^1.0.3"
 
 is-date-object@^1.0.1:
   version "1.0.5"
-  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
+  resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz"
   integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
   dependencies:
     has-tostringtag "^1.0.0"
 
 is-docker@^2.0.0, is-docker@^2.1.1:
   version "2.2.1"
-  resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
+  resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz"
   integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
 
 is-extglob@^2.1.1:
   version "2.1.1"
-  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
   integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
 
-is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
+is-glob@^4.0.0:
   version "4.0.1"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-glob@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz"
   integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
   dependencies:
     is-extglob "^2.1.1"
 
 is-glob@^4.0.3:
   version "4.0.3"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
   integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
   dependencies:
     is-extglob "^2.1.1"
 
+is-glob@~4.0.1:
+  version "4.0.1"
+  resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
 is-negative-zero@^2.0.2:
   version "2.0.2"
-  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
+  resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz"
   integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
 
 is-number-object@^1.0.4:
   version "1.0.6"
-  resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0"
+  resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz"
   integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==
   dependencies:
     has-tostringtag "^1.0.0"
 
 is-number@^7.0.0:
   version "7.0.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
 is-plain-object@^2.0.4:
   version "2.0.4"
-  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz"
   integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
   dependencies:
     isobject "^3.0.1"
 
 is-primitive@^3.0.1:
   version "3.0.1"
-  resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-3.0.1.tgz#98c4db1abff185485a657fc2905052b940524d05"
+  resolved "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz"
   integrity sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==
 
 is-regex@^1.1.4:
   version "1.1.4"
-  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
+  resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz"
   integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
   dependencies:
     call-bind "^1.0.2"
@@ -1516,96 +1335,96 @@ is-regex@^1.1.4:
 
 is-shared-array-buffer@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
+  resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz"
   integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
   dependencies:
     call-bind "^1.0.2"
 
 is-string@^1.0.5, is-string@^1.0.7:
   version "1.0.7"
-  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
+  resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz"
   integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
   dependencies:
     has-tostringtag "^1.0.0"
 
 is-symbol@^1.0.2, is-symbol@^1.0.3:
   version "1.0.4"
-  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
+  resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz"
   integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
   dependencies:
     has-symbols "^1.0.2"
 
 is-weakref@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
+  resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz"
   integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
   dependencies:
     call-bind "^1.0.2"
 
 is-wsl@^2.2.0:
   version "2.2.0"
-  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+  resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz"
   integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
   dependencies:
     is-docker "^2.0.0"
 
 isexe@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
   integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
 
 isobject@^3.0.1:
   version "3.0.1"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
   integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
 
 iterall@^1.2.1:
   version "1.3.0"
-  resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
+  resolved "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz"
   integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
 
 js-sdsl@^4.1.4:
   version "4.1.5"
-  resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a"
+  resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz"
   integrity sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==
 
 "js-tokens@^3.0.0 || ^4.0.0":
   version "4.0.0"
-  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
 js-yaml@^4.1.0:
   version "4.1.0"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
   integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
   dependencies:
     argparse "^2.0.1"
 
 json-schema-traverse@^0.4.1:
   version "0.4.1"
-  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
   integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
 
 json-stable-stringify-without-jsonify@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+  resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
   integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
 
 json5@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  resolved "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz"
   integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
   dependencies:
     minimist "^1.2.0"
 
 just-debounce-it@^3.1.1:
   version "3.1.1"
-  resolved "https://registry.yarnpkg.com/just-debounce-it/-/just-debounce-it-3.1.1.tgz#aa07c395d48c394233e4bafdcc49ed188fcf62a5"
+  resolved "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-3.1.1.tgz"
   integrity sha512-oPsuRyWp99LJaQ4KXC3A42tQNqkRTcPy0A8BCkRZ5cPCgsx81upB2KUrmHZvDUNhnCDKe7MshfTuWFQB9iXwDg==
 
 levn@^0.4.1:
   version "0.4.1"
-  resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+  resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz"
   integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
   dependencies:
     prelude-ls "^1.2.1"
@@ -1613,55 +1432,40 @@ levn@^0.4.1:
 
 linkify-it@^3.0.1:
   version "3.0.3"
-  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e"
+  resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz"
   integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==
   dependencies:
     uc.micro "^1.0.1"
 
 locate-path@^6.0.0:
   version "6.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+  resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz"
   integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
   dependencies:
     p-locate "^5.0.0"
 
-lodash.debounce@^4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
-  integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
-
-lodash.memoize@^4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
-  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
-
 lodash.merge@^4.6.2:
   version "4.6.2"
-  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+  resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-lodash.throttle@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
-  integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
-
 loose-envify@^1.1.0:
   version "1.4.0"
-  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+  resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
   integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
   dependencies:
     js-tokens "^3.0.0 || ^4.0.0"
 
 lru-cache@^6.0.0:
   version "6.0.0"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
   integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
   dependencies:
     yallist "^4.0.0"
 
 markdown-it@^12.2.0:
   version "12.3.2"
-  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90"
+  resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz"
   integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==
   dependencies:
     argparse "^2.0.1"
@@ -1672,22 +1476,22 @@ markdown-it@^12.2.0:
 
 mdurl@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+  resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz"
   integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
 
 merge2@^1.3.0, merge2@^1.4.1:
   version "1.4.1"
-  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
 meros@^1.1.4:
   version "1.1.4"
-  resolved "https://registry.yarnpkg.com/meros/-/meros-1.1.4.tgz#c17994d3133db8b23807f62bec7f0cb276cfd948"
+  resolved "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz"
   integrity sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==
 
 micromatch@^4.0.4:
   version "4.0.4"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
+  resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz"
   integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
   dependencies:
     braces "^3.0.1"
@@ -1695,81 +1499,92 @@ micromatch@^4.0.4:
 
 minimatch@^3.0.4:
   version "3.0.4"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz"
   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
   dependencies:
     brace-expansion "^1.1.7"
 
 minimatch@^3.1.2:
   version "3.1.2"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
   integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
   dependencies:
     brace-expansion "^1.1.7"
 
 minimist@^1.2.0:
   version "1.2.5"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
 minimist@^1.2.6:
   version "1.2.6"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+  resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz"
   integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
 
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
 ms@2.0.0:
   version "2.0.0"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
   integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
 
 ms@2.1.2:
   version "2.1.2"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-ms@^2.1.1:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
-  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-
 natural-compare@^1.4.0:
   version "1.4.0"
-  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+  resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
+"netbox-graphiql@file:/home/jstretch/projects/netbox/netbox/project-static/netbox-graphiql":
+  version "0.1.0"
+  resolved "file:netbox-graphiql"
+  dependencies:
+    graphiql "1.8.9"
+    graphql ">= v14.5.0 <= 15.5.0"
+    react "17.0.2"
+    react-dom "17.0.2"
+    subscriptions-transport-ws "0.9.18"
+    whatwg-fetch "3.6.2"
+
 normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
-  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
 
 nullthrows@^1.0.0:
   version "1.1.1"
-  resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
+  resolved "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz"
   integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
 
 object-assign@^4.1.1:
   version "4.1.1"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
   integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
 
 object-inspect@^1.12.2:
   version "1.12.2"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
+  resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz"
   integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
 
 object-inspect@^1.9.0:
   version "1.11.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
+  resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz"
   integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
 
 object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
-  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
 
 object.assign@^4.1.4:
   version "4.1.4"
-  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
+  resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz"
   integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
   dependencies:
     call-bind "^1.0.2"
@@ -1779,7 +1594,7 @@ object.assign@^4.1.4:
 
 object.values@^1.1.5:
   version "1.1.5"
-  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
+  resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz"
   integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==
   dependencies:
     call-bind "^1.0.2"
@@ -1788,14 +1603,14 @@ object.values@^1.1.5:
 
 once@^1.3.0:
   version "1.4.0"
-  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
   integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
   dependencies:
     wrappy "1"
 
 open@^8.4.0:
   version "8.4.0"
-  resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
+  resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz"
   integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==
   dependencies:
     define-lazy-prop "^2.0.0"
@@ -1804,7 +1619,7 @@ open@^8.4.0:
 
 optionator@^0.9.1:
   version "0.9.1"
-  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
+  resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz"
   integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
   dependencies:
     deep-is "^0.1.3"
@@ -1816,85 +1631,85 @@ optionator@^0.9.1:
 
 p-limit@^3.0.2:
   version "3.1.0"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
   integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
   dependencies:
     yocto-queue "^0.1.0"
 
 p-locate@^5.0.0:
   version "5.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+  resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz"
   integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
   dependencies:
     p-limit "^3.0.2"
 
 parent-module@^1.0.0:
   version "1.0.1"
-  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
   integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
   dependencies:
     callsites "^3.0.0"
 
 path-exists@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
   integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
 
 path-is-absolute@^1.0.0:
   version "1.0.1"
-  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
 
 path-key@^3.1.0:
   version "3.1.1"
-  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
   integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
 
 path-parse@^1.0.6, path-parse@^1.0.7:
   version "1.0.7"
-  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
 path-type@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
 
 picocolors@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
   integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
 
 picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
   version "2.3.0"
-  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
+  resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz"
   integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
 
 prelude-ls@^1.2.1:
   version "1.2.1"
-  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+  resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
   integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
 
 prettier-linter-helpers@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
+  resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz"
   integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
   dependencies:
     fast-diff "^1.1.2"
 
-prettier@^2.7.1:
+prettier@^2.7.1, prettier@>=2.0.0:
   version "2.7.1"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
+  resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz"
   integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
 
 punycode@^2.1.0:
   version "2.1.1"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
 query-string@^7.1.1:
   version "7.1.1"
-  resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1"
+  resolved "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz"
   integrity sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==
   dependencies:
     decode-uri-component "^0.2.0"
@@ -1904,21 +1719,21 @@ query-string@^7.1.1:
 
 queue-microtask@^1.2.2:
   version "1.2.3"
-  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+  resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
   integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 
-react-dom@17.0.2:
+"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", react-dom@17.0.2:
   version "17.0.2"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
+  resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
   integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
     scheduler "^0.20.2"
 
-react@17.0.2:
+"react@^16.8.0 || ^17.0.0 || ^18.0.0", react@17.0.2:
   version "17.0.2"
-  resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
+  resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
   integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
   dependencies:
     loose-envify "^1.1.0"
@@ -1926,14 +1741,14 @@ react@17.0.2:
 
 readdirp@~3.6.0:
   version "3.6.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz"
   integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
   dependencies:
     picomatch "^2.2.1"
 
 regexp.prototype.flags@^1.4.3:
   version "1.4.3"
-  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
+  resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz"
   integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
   dependencies:
     call-bind "^1.0.2"
@@ -1942,17 +1757,17 @@ regexp.prototype.flags@^1.4.3:
 
 regexpp@^3.2.0:
   version "3.2.0"
-  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
+  resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz"
   integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
 
 resolve-from@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
 resolve@^1.20.0:
   version "1.20.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+  resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz"
   integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
   dependencies:
     is-core-module "^2.2.0"
@@ -1960,7 +1775,7 @@ resolve@^1.20.0:
 
 resolve@^1.22.0, resolve@^1.22.1:
   version "1.22.1"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+  resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz"
   integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
   dependencies:
     is-core-module "^2.9.0"
@@ -1969,26 +1784,26 @@ resolve@^1.22.0, resolve@^1.22.1:
 
 reusify@^1.0.4:
   version "1.0.4"
-  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
 rimraf@^3.0.2:
   version "3.0.2"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz"
   integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
   dependencies:
     glob "^7.1.3"
 
 run-parallel@^1.1.9:
   version "1.2.0"
-  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+  resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
   integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
   dependencies:
     queue-microtask "^1.2.2"
 
 safe-regex-test@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295"
+  resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz"
   integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==
   dependencies:
     call-bind "^1.0.2"
@@ -1997,7 +1812,7 @@ safe-regex-test@^1.0.0:
 
 sass@^1.49.0, sass@^1.55.0:
   version "1.55.0"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.55.0.tgz#0c4d3c293cfe8f8a2e8d3b666e1cf1bff8065d1c"
+  resolved "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz"
   integrity sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==
   dependencies:
     chokidar ">=3.0.0 <4.0.0"
@@ -2006,7 +1821,7 @@ sass@^1.49.0, sass@^1.55.0:
 
 scheduler@^0.20.2:
   version "0.20.2"
-  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
+  resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz"
   integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
   dependencies:
     loose-envify "^1.1.0"
@@ -2014,19 +1829,19 @@ scheduler@^0.20.2:
 
 select@^1.1.2:
   version "1.1.2"
-  resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+  resolved "https://registry.npmjs.org/select/-/select-1.1.2.tgz"
   integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
 
 semver@^7.3.7:
   version "7.3.7"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
+  resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz"
   integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
   dependencies:
     lru-cache "^6.0.0"
 
 set-value@^4.1.0:
   version "4.1.0"
-  resolved "https://registry.yarnpkg.com/set-value/-/set-value-4.1.0.tgz#aa433662d87081b75ad88a4743bd450f044e7d09"
+  resolved "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz"
   integrity sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==
   dependencies:
     is-plain-object "^2.0.4"
@@ -2034,70 +1849,58 @@ set-value@^4.1.0:
 
 shebang-command@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
   integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
   dependencies:
     shebang-regex "^3.0.0"
 
 shebang-regex@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
 side-channel@^1.0.4:
   version "1.0.4"
-  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+  resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz"
   integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
   dependencies:
     call-bind "^1.0.0"
     get-intrinsic "^1.0.2"
     object-inspect "^1.9.0"
 
-simplebar@^5.3.9:
-  version "5.3.9"
-  resolved "https://registry.yarnpkg.com/simplebar/-/simplebar-5.3.9.tgz#168ea0eb6d52f29f03960e40d9b69a1b28cf6318"
-  integrity sha512-1vIIpjDvY9sVH14e0LGeiCiTFU3ILqAghzO6OI9axeG+mvU/vMSrvXeAXkBolqFFz3XYaY8n5ahH9MeP3sp2Ag==
-  dependencies:
-    "@juggle/resize-observer" "^3.3.1"
-    can-use-dom "^0.1.0"
-    core-js "^3.0.1"
-    lodash.debounce "^4.0.8"
-    lodash.memoize "^4.1.2"
-    lodash.throttle "^4.1.1"
-
 slash@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+  resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
 slash@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
+  resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz"
   integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
 
 slim-select@^1.27.1:
   version "1.27.1"
-  resolved "https://registry.yarnpkg.com/slim-select/-/slim-select-1.27.1.tgz#aab08c1f0428fc2c32b0376857f3da9b1fbdcb2f"
+  resolved "https://registry.npmjs.org/slim-select/-/slim-select-1.27.1.tgz"
   integrity sha512-LvJ02cKKk6/jSHIcQv7dZwkQSXHLCVQR3v3lo8RJUssUUcmKPkpBmTpQ8au8KSMkxwca9+yeg+dO0iHAaVr5Aw==
 
 "source-map-js@>=0.6.2 <2.0.0":
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
 
 split-on-first@^1.0.0:
   version "1.1.0"
-  resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
+  resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz"
   integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
 
 strict-uri-encode@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
+  resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz"
   integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
 
 string.prototype.trimend@^1.0.5:
   version "1.0.5"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0"
+  resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz"
   integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==
   dependencies:
     call-bind "^1.0.2"
@@ -2106,7 +1909,7 @@ string.prototype.trimend@^1.0.5:
 
 string.prototype.trimstart@^1.0.5:
   version "1.0.5"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef"
+  resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz"
   integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==
   dependencies:
     call-bind "^1.0.2"
@@ -2115,24 +1918,24 @@ string.prototype.trimstart@^1.0.5:
 
 strip-ansi@^6.0.1:
   version "6.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
   dependencies:
     ansi-regex "^5.0.1"
 
 strip-bom@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+  resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz"
   integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
 
 strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
   version "3.1.1"
-  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+  resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
 subscriptions-transport-ws@0.9.18:
   version "0.9.18"
-  resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz#bcf02320c911fbadb054f7f928e51c6041a37b97"
+  resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz"
   integrity sha512-tztzcBTNoEbuErsVQpTN2xUNN/efAZXyCyL5m3x4t6SKrEiTL2N8SaKWBFWM4u56pL79ULif3zjyeq+oV+nOaA==
   dependencies:
     backo2 "^1.0.2"
@@ -2143,24 +1946,24 @@ subscriptions-transport-ws@0.9.18:
 
 supports-color@^7.1.0:
   version "7.2.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
   integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
   dependencies:
     has-flag "^4.0.0"
 
 supports-preserve-symlinks-flag@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
 symbol-observable@^1.0.4:
   version "1.2.0"
-  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+  resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz"
   integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
 
 synckit@^0.8.3:
   version "0.8.4"
-  resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.4.tgz#0e6b392b73fafdafcde56692e3352500261d64ec"
+  resolved "https://registry.npmjs.org/synckit/-/synckit-0.8.4.tgz"
   integrity sha512-Dn2ZkzMdSX827QbowGbU/4yjWuvNaCoScLLoMo/yKbu+P4GBR6cRGKZH27k6a9bRzdqcyd1DE96pQtQ6uNkmyw==
   dependencies:
     "@pkgr/utils" "^2.3.1"
@@ -2168,22 +1971,22 @@ synckit@^0.8.3:
 
 tapable@^2.2.0:
   version "2.2.1"
-  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
+  resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz"
   integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
 
 text-table@^0.2.0:
   version "0.2.0"
-  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+  resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
 
 tiny-emitter@^2.0.0:
   version "2.1.0"
-  resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
+  resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz"
   integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
 
 tiny-glob@^0.2.9:
   version "0.2.9"
-  resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2"
+  resolved "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz"
   integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==
   dependencies:
     globalyzer "0.1.0"
@@ -2191,19 +1994,19 @@ tiny-glob@^0.2.9:
 
 to-regex-range@^5.0.1:
   version "5.0.1"
-  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
   integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
   dependencies:
     is-number "^7.0.0"
 
 toggle-selection@^1.0.6:
   version "1.0.6"
-  resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+  resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz"
   integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
 
 tsconfig-paths@^3.14.1:
   version "3.14.1"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"
+  resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz"
   integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==
   dependencies:
     "@types/json5" "^0.0.29"
@@ -2213,46 +2016,56 @@ tsconfig-paths@^3.14.1:
 
 tslib@^1.8.1:
   version "1.14.1"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+  resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
 tslib@^2.4.0:
   version "2.4.0"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
+  resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz"
   integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
 
 tsutils@^3.21.0:
   version "3.21.0"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+  resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
   integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
   dependencies:
     tslib "^1.8.1"
 
 type-check@^0.4.0, type-check@~0.4.0:
   version "0.4.0"
-  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+  resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
   integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
   dependencies:
     prelude-ls "^1.2.1"
 
 type-fest@^0.20.2:
   version "0.20.2"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+  resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
   integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
 
-typescript@~4.8.4:
+typeface-inter@^3.18.1:
+  version "3.18.1"
+  resolved "https://registry.npmjs.org/typeface-inter/-/typeface-inter-3.18.1.tgz"
+  integrity sha512-c+TBanYFCvmg3j5vPk+zxK4ocMZbPxMEmjnwG7rPQoV87xvQ6b07VbAOC0Va0XBbbZCGw6cWNeFuLeg1YQru3Q==
+
+typeface-roboto-mono@^1.1.13:
+  version "1.1.13"
+  resolved "https://registry.npmjs.org/typeface-roboto-mono/-/typeface-roboto-mono-1.1.13.tgz"
+  integrity sha512-pnzDc70b7ywJHin/BUFL7HZX8DyOTBLT2qxlJ92eH1UJOFcENIBXa9IZrxsJX/gEKjbEDKhW5vz/TKRBNk/ufQ==
+
+"typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@~4.8.4:
   version "4.8.4"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
+  resolved "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz"
   integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
 
 uc.micro@^1.0.1, uc.micro@^1.0.5:
   version "1.0.6"
-  resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
+  resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz"
   integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
 
 unbox-primitive@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
+  resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz"
   integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
   dependencies:
     call-bind "^1.0.2"
@@ -2262,29 +2075,29 @@ unbox-primitive@^1.0.2:
 
 uri-js@^4.2.2:
   version "4.4.1"
-  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
   integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
   dependencies:
     punycode "^2.1.0"
 
 vscode-languageserver-types@^3.15.1:
   version "3.17.2"
-  resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz#b2c2e7de405ad3d73a883e91989b850170ffc4f2"
+  resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz"
   integrity sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==
 
 vscode-languageserver-types@^3.17.1:
   version "3.17.3"
-  resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64"
+  resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz"
   integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==
 
 whatwg-fetch@3.6.2:
   version "3.6.2"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
+  resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz"
   integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
 
 which-boxed-primitive@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
+  resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz"
   integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
   dependencies:
     is-bigint "^1.0.1"
@@ -2295,34 +2108,34 @@ which-boxed-primitive@^1.0.2:
 
 which@^2.0.1:
   version "2.0.2"
-  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
   dependencies:
     isexe "^2.0.0"
 
 word-wrap@^1.2.3:
   version "1.2.3"
-  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
 
 wrappy@1:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
 ws@^5.2.0:
   version "5.2.3"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d"
+  resolved "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz"
   integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==
   dependencies:
     async-limiter "~1.0.0"
 
 yallist@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
 yocto-queue@^0.1.0:
   version "0.1.0"
-  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
diff --git a/netbox/templates/account/base.html b/netbox/templates/account/base.html
index 6c1e9f028..51076f781 100644
--- a/netbox/templates/account/base.html
+++ b/netbox/templates/account/base.html
@@ -1,8 +1,8 @@
-{% extends 'base/layout.html' %}
+{% extends 'generic/_base.html' %}
 {% load i18n %}
 
 {% block tabs %}
-