mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-29 05:08:15 +01:00
Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e93e9ac4a0 | ||
|
|
97cd6b89fd | ||
|
|
3684e011e6 | ||
|
|
a9fd5bbf55 | ||
|
|
e325a4b2e0 | ||
|
|
a150e5d561 | ||
|
|
8282a6ddfe | ||
|
|
07b1362b5e | ||
|
|
e3e351d1f0 | ||
|
|
50839fcb6b | ||
|
|
cac92352ca | ||
|
|
0464dacf7e | ||
|
|
cf62178471 | ||
|
|
c3276b7d2e | ||
|
|
7bae448eaf | ||
|
|
5ebdb7c9d2 | ||
|
|
0157ac6c9b | ||
|
|
d23b9370f6 | ||
|
|
c2d67fa17e | ||
|
|
4f225b4e56 | ||
|
|
263664ae52 | ||
|
|
0238aeec22 | ||
|
|
3fee28cd5e | ||
|
|
515d041560 | ||
|
|
8bea914163 | ||
|
|
420613daed | ||
|
|
fd013d6c5c | ||
|
|
a7f83de8c4 | ||
|
|
ee0af15073 | ||
|
|
35e2cf9cec | ||
|
|
1d2ea90fd4 | ||
|
|
dab27695b9 | ||
|
|
d4dd86eb04 | ||
|
|
5bd4fc862d | ||
|
|
85f8364cd7 | ||
|
|
8903e4649c | ||
|
|
38a26a7908 | ||
|
|
96802b4edb | ||
|
|
e656e2da24 | ||
|
|
c457f01b19 | ||
|
|
277b7039d8 | ||
|
|
e2dfa63df6 | ||
|
|
370c1209f4 | ||
|
|
09d6b9c62f | ||
|
|
4747cdef0b | ||
|
|
f3f1aa3841 | ||
|
|
727cb65c50 | ||
|
|
872af72b8e | ||
|
|
5a9f9af2fa | ||
|
|
09d36469dd | ||
|
|
8789aaaa39 | ||
|
|
d5c1a5acda | ||
|
|
6feb8bf0e3 | ||
|
|
9e54cfe340 | ||
|
|
6a663e2a3e | ||
|
|
7c9a77b77f | ||
|
|
81fe12a7d9 | ||
|
|
9c7002f691 | ||
|
|
20967bf88d | ||
|
|
34d20fccd5 | ||
|
|
f6c1642116 | ||
|
|
b7b0ab16f5 | ||
|
|
6ae3af2f26 | ||
|
|
6c845bd5de | ||
|
|
597fc926c0 | ||
|
|
fa2b3bcfcc | ||
|
|
dc173a5508 | ||
|
|
408f8b4964 | ||
|
|
f949aa334b | ||
|
|
8bfcb1c816 | ||
|
|
630c6fb43d | ||
|
|
c8b4faefcb | ||
|
|
cbf84a6b95 | ||
|
|
173c339993 | ||
|
|
5ebdf7fc0f | ||
|
|
0d30ab3462 | ||
|
|
17ddbdd3b8 | ||
|
|
cb59f6e6f7 | ||
|
|
93cebae55c | ||
|
|
2620d6067a | ||
|
|
3c9d173139 | ||
|
|
181fe0a3cc | ||
|
|
70b2451209 | ||
|
|
dab07d653f | ||
|
|
8b2f9bf700 | ||
|
|
8f54724ac1 | ||
|
|
c9452db6cf | ||
|
|
a9dadfd179 | ||
|
|
5019a67a61 | ||
|
|
95347cfd0f | ||
|
|
eb74393070 | ||
|
|
874677b359 | ||
|
|
3cde4da4a9 | ||
|
|
0b1b9caea4 | ||
|
|
2969c4188c | ||
|
|
68013cb554 | ||
|
|
5c272f8e6e | ||
|
|
e51d67c72a | ||
|
|
0e0d6172a4 | ||
|
|
b3fbcb3afc | ||
|
|
4f60b26bf3 | ||
|
|
0bc17850fd | ||
|
|
30b9fcf4f8 | ||
|
|
ef5c0256f8 | ||
|
|
db081e2b5e | ||
|
|
11f13bf2a4 | ||
|
|
7b5e8d5f2a | ||
|
|
303c1ce00c | ||
|
|
b4240cdd67 | ||
|
|
1e7a71969e | ||
|
|
f3d1924453 | ||
|
|
4d55d7d964 | ||
|
|
d8c7282fdb | ||
|
|
cc72a58c1e | ||
|
|
36df9228a6 | ||
|
|
424dda5be6 | ||
|
|
3028f262cc | ||
|
|
11cadf3a8a | ||
|
|
954d0cfcd0 | ||
|
|
0830ebb34a | ||
|
|
95cb7b2c34 | ||
|
|
dde77c83b4 | ||
|
|
e216bebd41 | ||
|
|
d39acfd3f2 | ||
|
|
4ea4e57f33 | ||
|
|
377543cd9c | ||
|
|
a8827c8472 | ||
|
|
b2ca6df50a | ||
|
|
ab6ddd50a8 | ||
|
|
499da4fdcf | ||
|
|
4fa396716e | ||
|
|
6f3a2a599f | ||
|
|
960d2b82b7 | ||
|
|
f2e1de027f | ||
|
|
bf97138c78 | ||
|
|
30d711d24a | ||
|
|
2a8bec1cbf | ||
|
|
013139aa20 | ||
|
|
4ca1494127 | ||
|
|
70311a9db5 | ||
|
|
dd413b248a |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -26,7 +26,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.7
|
||||
placeholder: v4.0.10
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.7
|
||||
placeholder: v4.0.10
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
21
.github/workflows/auto-assign-issue.yml
vendored
21
.github/workflows/auto-assign-issue.yml
vendored
@@ -1,21 +0,0 @@
|
||||
# auto-assign-issue (https://github.com/marketplace/actions/auto-assign-issue)
|
||||
name: Issue assignment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
auto-assign:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: pozil/auto-assign-issue@v2
|
||||
if: "contains(github.event.issue.labels.*.name, 'status: needs triage')"
|
||||
with:
|
||||
# Weighted assignments
|
||||
assignees: arthanson:3, jeffgdotorg:3, jeremystretch:3, DanSheps
|
||||
numOfAssignee: 1
|
||||
abortIfPreviousAssignees: true
|
||||
@@ -40,7 +40,7 @@ NetBox users are welcome to participate in either role, on stage or in the crowd
|
||||
|
||||
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's likely that the bug has already been fixed.
|
||||
|
||||
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
|
||||
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the bottom left corner of the issue and add a thumbs up ( :thumbsup: ). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
|
||||
|
||||
* If you can't find any existing issues (open or closed) that seem to match yours, you're welcome to [submit a new bug report](https://github.com/netbox-community/netbox/issues/new?label=type%3A+bug&template=bug_report.yaml). Be sure to complete the entire report template, including detailed steps that someone triaging your issue can follow to confirm the reported behavior. (If we're not able to replicate the bug based on the information provided, we'll ask for additional detail.)
|
||||
|
||||
@@ -56,7 +56,9 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
|
||||
|
||||
## :bulb: Feature Requests
|
||||
|
||||
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature.
|
||||
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up ( :thumbsup: ). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature.
|
||||
|
||||
* Please don't submit duplicate issues! Sometimes we reject feature requests, for various reasons. Even if you disagree with those reasons, please **do not** submit a duplicate feature request. It is very disrepectful of the maintainers' time, and you may be barred from opening future issues.
|
||||
|
||||
* If you have a rough idea that's not quite ready for formal submission yet, start a [GitHub discussion](https://github.com/netbox-community/netbox/discussions) instead. This is a great way to test the viability and narrow down the scope of a new feature prior to submitting a formal proposal, and can serve to generate interest in your idea from other community members.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-10-blue" alt="Languages supported" /></a>
|
||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
|
||||
<p></p>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ Administrators are encouraged to adhere to industry best practices concerning th
|
||||
|
||||
## Reporting a Suspected Vulnerability
|
||||
|
||||
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so via email. Please note that any reported vulnerabilities **MUST** meet all the following conditions:
|
||||
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so by emailing `security@netboxlabs.com`. Please ensure that your report meets all the following conditions:
|
||||
|
||||
* Affects the most recent stable release of NetBox, or a current beta release
|
||||
* Affects a NetBox instance installed and configured per the official documentation
|
||||
@@ -24,7 +24,7 @@ If you believe you've uncovered a security vulnerability and wish to report it c
|
||||
|
||||
Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous.
|
||||
|
||||
If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
|
||||
For any security concerns regarding the community-maintained Docker image for NetBox, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
|
||||
|
||||
### Bug Bounties
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ django-cors-headers
|
||||
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
|
||||
# Pinned for DNS looukp bug; see https://github.com/netbox-community/netbox/issues/16454
|
||||
# and https://github.com/jazzband/django-debug-toolbar/issues/1927
|
||||
django-debug-toolbar==4.3.0
|
||||
django-debug-toolbar
|
||||
|
||||
# Library for writing reusable URL query filters
|
||||
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
|
||||
|
||||
@@ -377,6 +377,7 @@
|
||||
"ieee802.11ad",
|
||||
"ieee802.11ax",
|
||||
"ieee802.11ay",
|
||||
"ieee802.11be",
|
||||
"ieee802.15.1",
|
||||
"other-wireless",
|
||||
"gsm",
|
||||
|
||||
@@ -40,3 +40,22 @@ REMOTE_AUTH_BACKEND = 'social_core.backends.google.GoogleOAuth2'
|
||||
NetBox supports single sign-on authentication via the [python-social-auth](https://github.com/python-social-auth) library. To enable SSO, specify the path to the desired authentication backend within the `social_core` Python package. Please see the complete list of [supported authentication backends](https://github.com/python-social-auth/social-core/tree/master/social_core/backends) for the available options.
|
||||
|
||||
Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter. (NetBox's default pipeline is defined in `netbox/settings.py` for your reference.)
|
||||
|
||||
#### Configuring the SSO module's appearance
|
||||
|
||||
The way a remote authentication backend is displayed to the user on the login
|
||||
page may be adjusted via the `SOCIAL_AUTH_BACKEND_ATTRS` parameter, defaulting
|
||||
to an empty dictionary. This dictionary maps a `social_core` module's name (ie.
|
||||
`REMOTE_AUTH_BACKEND.name`) to a couple of parameters, `(display_name, icon)`.
|
||||
|
||||
The `display_name` is the name displayed to the user on the login page. The
|
||||
icon may either be the URL of an icon; refer to a [Material Design
|
||||
Icons](https://github.com/google/material-design-icons) icon's name; or be
|
||||
`None` for no icon.
|
||||
|
||||
For instance, the OIDC backend may be customized with
|
||||
```python
|
||||
SOCIAL_AUTH_BACKEND_ATTRS = {
|
||||
'oidc': ("My awesome SSO", "login"),
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Default: False
|
||||
|
||||
This setting enables debugging. Debugging should be enabled only during development or troubleshooting. Note that only
|
||||
clients which access NetBox from a recognized [internal IP address](#internal_ips) will see debugging tools in the user
|
||||
clients which access NetBox from a recognized [internal IP address](./system.md#internal_ips) will see debugging tools in the user
|
||||
interface.
|
||||
|
||||
!!! warning
|
||||
|
||||
@@ -83,7 +83,7 @@ Default: `('127.0.0.1', '::1')`
|
||||
|
||||
A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
|
||||
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
|
||||
addresses (and [`DEBUG`](#debug) is true).
|
||||
addresses (and [`DEBUG`](./development.md#debug) is true).
|
||||
|
||||
---
|
||||
|
||||
@@ -106,7 +106,7 @@ JINJA2_FILTERS = {
|
||||
|
||||
## LOGGING
|
||||
|
||||
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](#admins).
|
||||
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](./development.md#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](./miscellaneous.md#admins).
|
||||
|
||||
The Django framework on which NetBox runs allows for the customization of logging format and destination. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/stable/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a local file:
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Sometimes it becomes necessary to constrain dependencies to a particular version
|
||||
djangorestframework==3.8.1
|
||||
```
|
||||
|
||||
These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-requirements) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
|
||||
These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-python-dependencies) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
|
||||
|
||||
### Close the Release Milestone
|
||||
|
||||
@@ -113,7 +113,7 @@ Create a [new release](https://github.com/netbox-community/netbox/releases/new)
|
||||
* **Tag:** Current version (e.g. `v3.3.1`)
|
||||
* **Target:** `master`
|
||||
* **Title:** Version and date (e.g. `v3.3.1 - 2022-08-25`)
|
||||
* **Description:** Copy from the pull request body
|
||||
* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
|
||||
|
||||
Once created, the release will become available for users to install.
|
||||
|
||||
@@ -135,6 +135,6 @@ First, run the `build-site` action, by navigating to Actions > build-site > Run
|
||||
|
||||
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
|
||||
|
||||
Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _CDN_ in the left-nav, click the _Clear CDN cache_ button, and confirm the clear operation.
|
||||
Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _Cache_ in the left-nav, click the _Clear Cache_ button, and confirm the clear operation.
|
||||
|
||||
Finally, verify that the documentation at <https://netboxlabs.com/docs/netbox/en/stable/> has been updated.
|
||||
|
||||
@@ -41,7 +41,7 @@ Line breaks are permitted following binary operators.
|
||||
|
||||
### Enforcing Code Style
|
||||
|
||||
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#2-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
|
||||
```
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
|
||||
@@ -20,6 +20,8 @@ Then, commit the change and push to the `develop` branch on GitHub. Any new stri
|
||||
|
||||
Typically, translated strings need to be updated only as part of the NetBox [release process](./release-checklist.md).
|
||||
|
||||
Check the Transifex dashboard for languages that are not marked _ready for use_, being sure to click _Show all languages_ if it appears at the bottom of the list. Use machine translation to round out any not-ready languages. It's not necessary to review the machine translation immediately as the translation teams will handle that aspect; the goal at this stage is to get translations included in the Transifex pull request.
|
||||
|
||||
To update translated strings, start by initiating a sync from Transifex. From the Transifex dashboard, navigate to Settings > Integrations > GitHub > Manage, and click the **Manual Sync** button at top right.
|
||||
|
||||

|
||||
|
||||
@@ -84,11 +84,11 @@ To create a viewset for a plugin model, subclass `NetBoxModelViewSet` in `api/vi
|
||||
|
||||
```python
|
||||
# api/views.py
|
||||
from netbox.api.viewsets import ModelViewSet
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from my_plugin.models import MyModel
|
||||
from .serializers import MyModelSerializer
|
||||
|
||||
class MyModelViewSet(ModelViewSet):
|
||||
class MyModelViewSet(NetBoxModelViewSet):
|
||||
queryset = MyModel.objects.all()
|
||||
serializer_class = MyModelSerializer
|
||||
```
|
||||
|
||||
@@ -1,5 +1,90 @@
|
||||
# NetBox v4.0
|
||||
|
||||
## v4.0.10 (2024-08-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16857](https://github.com/netbox-community/netbox/issues/16857) - Scroll long rendered Markdown content within tables
|
||||
* [#16905](https://github.com/netbox-community/netbox/issues/16905) - Enable filtering of device components by device status
|
||||
* [#16949](https://github.com/netbox-community/netbox/issues/16949) - Add device count column to sites table
|
||||
* [#17072](https://github.com/netbox-community/netbox/issues/17072) - Linkify email addresses & phone numbers in contact assignments list
|
||||
* [#17177](https://github.com/netbox-community/netbox/issues/17177) - Add facility field to locations filter form
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16292](https://github.com/netbox-community/netbox/issues/16292) - Ensure consistent evaluation of queryset for both individual and list GraphQL API queries
|
||||
* [#16385](https://github.com/netbox-community/netbox/issues/16385) - Restore support for white, gray, and black background colors
|
||||
* [#16640](https://github.com/netbox-community/netbox/issues/16640) - Fix potential corruption of JSON values in custom fields that are not UI-editable
|
||||
* [#16670](https://github.com/netbox-community/netbox/issues/16670) - Fix conflicts within OpenAPI schema definition regarding nested serializers
|
||||
* [#16733](https://github.com/netbox-community/netbox/issues/16733) - Fix bulk edit/delete of objects when using "select all" widget
|
||||
* [#16756](https://github.com/netbox-community/netbox/issues/16756) - Fix dynamic pagination of custom script results table
|
||||
* [#16825](https://github.com/netbox-community/netbox/issues/16825) - Avoid `NoReverseMatch` exception when displaying count of related object type with no list view
|
||||
* [#16946](https://github.com/netbox-community/netbox/issues/16946) - GraphQL API requests with an invalid filter should return an empty set
|
||||
* [#16959](https://github.com/netbox-community/netbox/issues/16959) - Fix function of "reset" button on objects filter form
|
||||
* [#16973](https://github.com/netbox-community/netbox/issues/16973) - Fix support for evaluating user token (`$user`) against custom field values in permission constraints
|
||||
* [#17007](https://github.com/netbox-community/netbox/issues/17007) - Center SSO authentication icon when backend is unnamed
|
||||
* [#17070](https://github.com/netbox-community/netbox/issues/17070) - Image height & width values should not be required when creating an image attachment via the REST API
|
||||
* [#17108](https://github.com/netbox-community/netbox/issues/17108) - Ensure template date & time filters always return localtime-aware values
|
||||
* [#17117](https://github.com/netbox-community/netbox/issues/17117) - Work around Safari rendering bug
|
||||
* [#17186](https://github.com/netbox-community/netbox/issues/17186) - Fix display of custom links with default style under dark mode
|
||||
* [#17219](https://github.com/netbox-community/netbox/issues/17219) - Fix system config view exception when custom validator classes are employed
|
||||
* [#17230](https://github.com/netbox-community/netbox/issues/17230) - Ensure consistent rendering for all dashboard widget colors
|
||||
* [#17256](https://github.com/netbox-community/netbox/issues/17256) - Fix VLAN group scope selection for non-English languages
|
||||
* [#17278](https://github.com/netbox-community/netbox/issues/17278) - Ensure hierarchy is recalculated when bulk editing recursively nested object types (e.g. tenant groups)
|
||||
* [#17279](https://github.com/netbox-community/netbox/issues/17279) - Do not regenerate key when updating a token via REST API
|
||||
* [#17286](https://github.com/netbox-community/netbox/issues/17286) - Fix exception when adding member device to virtual chassis via web UI
|
||||
|
||||
---
|
||||
|
||||
## v4.0.9 (2024-08-14)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16692](https://github.com/netbox-community/netbox/issues/16692) - Enable modifying VLAN assignment while bulk editing prefixes
|
||||
* [#17006](https://github.com/netbox-community/netbox/issues/17006) - Add IEEE 802.11be interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#13459](https://github.com/netbox-community/netbox/issues/13459) - Correct OpenAPI schema type for `TreeNodeMultipleChoiceFilter`
|
||||
* [#16073](https://github.com/netbox-community/netbox/issues/16073) - Respect default values for custom fields during bulk import of objects
|
||||
* [#16176](https://github.com/netbox-community/netbox/issues/16176) - Restore ability to select multiple terminating devices when connecting a cable
|
||||
* [#16871](https://github.com/netbox-community/netbox/issues/16871) - Sanitize device ID query parameter when bulk editing components to prevent exception
|
||||
* [#17038](https://github.com/netbox-community/netbox/issues/17038) - Fix AttributeError exception when attempting to export system status data
|
||||
* [#17064](https://github.com/netbox-community/netbox/issues/17064) - Fix misaligned text within rendered Markdown code blocks
|
||||
* [#17124](https://github.com/netbox-community/netbox/issues/17124) - `BaseTable` should follow reverse one-to-one relationships when prefetching related objects
|
||||
* [#17131](https://github.com/netbox-community/netbox/issues/17131) - Fix exception when creating object-type custom field without selecting related object type
|
||||
* [#17144](https://github.com/netbox-community/netbox/issues/17144) - Avoid showing duplicated pop-up messages
|
||||
|
||||
---
|
||||
|
||||
## v4.0.8 (2024-07-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14640](https://github.com/netbox-community/netbox/issues/14640) - Add Dutch language support
|
||||
* [#14792](https://github.com/netbox-community/netbox/issues/14792) - Add Polish language support
|
||||
* [#15375](https://github.com/netbox-community/netbox/issues/15375) - Enable customization of SSO backend name & icon
|
||||
* [#15660](https://github.com/netbox-community/netbox/issues/15660) - Add Czech language support
|
||||
* [#15696](https://github.com/netbox-community/netbox/issues/15696) - Add Danish language support
|
||||
* [#16793](https://github.com/netbox-community/netbox/issues/16793) - Add Italian language support
|
||||
* [#16933](https://github.com/netbox-community/netbox/issues/16933) - Enable toggling true/false marks on BooleanColumn
|
||||
* [#16943](https://github.com/netbox-community/netbox/issues/16943) - Expand navigation breadcrumbs on job view to include the parent object
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16357](https://github.com/netbox-community/netbox/issues/16357) - Replicate assigned type & tenant for cable when clicking "create an add another"
|
||||
* [#16402](https://github.com/netbox-community/netbox/issues/16402) - Remove inoperative links from report result view
|
||||
* [#16536](https://github.com/netbox-community/netbox/issues/16536) - Revert `role` & `role_id` filters for device components to `device_role` & `device_role_id` to avoid conflict with inventory item `role` field
|
||||
* [#16624](https://github.com/netbox-community/netbox/issues/16624) - Correct OpenAPI schema definitions for several fields
|
||||
* [#16760](https://github.com/netbox-community/netbox/issues/16760) - Fix data source syncing using git via a local path
|
||||
* [#16819](https://github.com/netbox-community/netbox/issues/16819) - Highlight parent device in rack when viewing child device
|
||||
* [#16838](https://github.com/netbox-community/netbox/issues/16838) - ActionsColumn should render extra buttons even when no stock actions are enabled
|
||||
* [#16867](https://github.com/netbox-community/netbox/issues/16867) - Fix exception when a dashboard list widget references a model which has been removed
|
||||
* [#16963](https://github.com/netbox-community/netbox/issues/16963) - Fix filtering of "accounts" link under providers list
|
||||
* [#16964](https://github.com/netbox-community/netbox/issues/16964) - Ensure configured password validators are enforced
|
||||
|
||||
---
|
||||
|
||||
## v4.0.7 (2024-07-09)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -44,10 +44,20 @@ class LoginView(View):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def gen_auth_data(self, name, url, params):
|
||||
display_name, icon_name = get_auth_backend_display(name)
|
||||
display_name, icon_source = get_auth_backend_display(name)
|
||||
|
||||
icon_name = None
|
||||
icon_img = None
|
||||
if icon_source:
|
||||
if '://' in icon_source:
|
||||
icon_img = icon_source
|
||||
else:
|
||||
icon_name = icon_source
|
||||
|
||||
return {
|
||||
'display_name': display_name,
|
||||
'icon_name': icon_name,
|
||||
'icon_img': icon_img,
|
||||
'url': f'{url}?{urlencode(params)}',
|
||||
}
|
||||
|
||||
@@ -99,7 +109,7 @@ class LoginView(View):
|
||||
# Authenticate user
|
||||
auth_login(request, form.get_user())
|
||||
logger.info(f"User {request.user} successfully authenticated")
|
||||
messages.success(request, f"Logged in as {request.user}.")
|
||||
messages.success(request, _("Logged in as {user}.").format(user=request.user))
|
||||
|
||||
# Ensure the user has a UserConfig defined. (This should normally be handled by
|
||||
# create_userconfig() on user creation.)
|
||||
@@ -149,7 +159,7 @@ class LogoutView(View):
|
||||
username = request.user
|
||||
auth_logout(request)
|
||||
logger.info(f"User {username} has logged out")
|
||||
messages.info(request, "You have logged out.")
|
||||
messages.info(request, _("You have logged out."))
|
||||
|
||||
# Delete session key & language cookies (if set) upon logout
|
||||
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
|
||||
@@ -224,7 +234,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
# LDAP users cannot change their password here
|
||||
if getattr(request.user, 'ldap_username', None):
|
||||
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
|
||||
messages.warning(request, _("LDAP-authenticated user credentials cannot be changed within NetBox."))
|
||||
return redirect('account:profile')
|
||||
|
||||
form = PasswordChangeForm(user=request.user)
|
||||
@@ -239,7 +249,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
update_session_auth_hash(request, form.user)
|
||||
messages.success(request, "Your password has been changed successfully.")
|
||||
messages.success(request, _("Your password has been changed successfully."))
|
||||
return redirect('account:profile')
|
||||
|
||||
return render(request, self.template_name, {
|
||||
|
||||
@@ -3,38 +3,25 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from circuits import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class CircuitsQuery:
|
||||
@strawberry.field
|
||||
def circuit(self, id: int) -> CircuitType:
|
||||
return models.Circuit.objects.get(pk=id)
|
||||
circuit: CircuitType = strawberry_django.field()
|
||||
circuit_list: List[CircuitType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_termination(self, id: int) -> CircuitTerminationType:
|
||||
return models.CircuitTermination.objects.get(pk=id)
|
||||
circuit_termination: CircuitTerminationType = strawberry_django.field()
|
||||
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_type(self, id: int) -> CircuitTypeType:
|
||||
return models.CircuitType.objects.get(pk=id)
|
||||
circuit_type: CircuitTypeType = strawberry_django.field()
|
||||
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider(self, id: int) -> ProviderType:
|
||||
return models.Provider.objects.get(pk=id)
|
||||
provider: ProviderType = strawberry_django.field()
|
||||
provider_list: List[ProviderType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider_account(self, id: int) -> ProviderAccountType:
|
||||
return models.ProviderAccount.objects.get(pk=id)
|
||||
provider_account: ProviderAccountType = strawberry_django.field()
|
||||
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider_network(self, id: int) -> ProviderNetworkType:
|
||||
return models.ProviderNetwork.objects.get(pk=id)
|
||||
provider_network: ProviderNetworkType = strawberry_django.field()
|
||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
||||
|
||||
@@ -25,7 +25,7 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
account_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('accounts__count'),
|
||||
viewname='circuits:provideraccount_list',
|
||||
url_params={'account_id': 'pk'},
|
||||
url_params={'provider_id': 'pk'},
|
||||
verbose_name=_('Account Count')
|
||||
)
|
||||
asns = columns.ManyToManyColumn(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.views import PathTraceView
|
||||
from netbox.views import generic
|
||||
@@ -326,7 +327,9 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
|
||||
# Circuit must have at least one termination to swap
|
||||
if not circuit.termination_a and not circuit.termination_z:
|
||||
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
|
||||
messages.error(request, _(
|
||||
"No terminations have been defined for circuit {circuit}."
|
||||
).format(circuit=circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
return render(request, 'circuits/circuit_terminations_swap.html', {
|
||||
@@ -374,7 +377,7 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
circuit.termination_z = None
|
||||
circuit.save()
|
||||
|
||||
messages.success(request, f"Swapped terminations for circuit {circuit}.")
|
||||
messages.success(request, _("Swapped terminations for circuit {circuit}.").format(circuit=circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
return render(request, 'circuits/circuit_terminations_swap.html', {
|
||||
|
||||
@@ -126,9 +126,18 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
|
||||
return response_serializers
|
||||
|
||||
def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str:
|
||||
name = super()._get_serializer_name(serializer, direction, bypass_extensions)
|
||||
|
||||
# If this serializer is nested, prepend its name with "Brief"
|
||||
if getattr(serializer, 'nested', False):
|
||||
name = f'Brief{name}'
|
||||
|
||||
return name
|
||||
|
||||
def get_serializer_ref_name(self, serializer):
|
||||
# from drf-yasg.utils
|
||||
"""Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
|
||||
"""Get serializer's ref_name
|
||||
:param serializer: Serializer instance
|
||||
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
|
||||
:rtype: str or None
|
||||
@@ -137,8 +146,6 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
serializer_name = type(serializer).__name__
|
||||
if hasattr(serializer_meta, 'ref_name'):
|
||||
ref_name = serializer_meta.ref_name
|
||||
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
|
||||
ref_name = None
|
||||
else:
|
||||
ref_name = serializer_name
|
||||
if ref_name.endswith('Serializer'):
|
||||
|
||||
@@ -84,9 +84,7 @@ class GitBackend(DataBackend):
|
||||
clone_args = {
|
||||
"branch": self.params.get('branch'),
|
||||
"config": self.config,
|
||||
"depth": 1,
|
||||
"errstream": porcelain.NoneStream(),
|
||||
"quiet": True,
|
||||
}
|
||||
|
||||
if self.url_scheme in ('http', 'https'):
|
||||
@@ -97,6 +95,9 @@ class GitBackend(DataBackend):
|
||||
"password": self.params.get('password'),
|
||||
}
|
||||
)
|
||||
if self.url_scheme:
|
||||
clone_args["quiet"] = True
|
||||
clone_args["depth"] = 1
|
||||
|
||||
logger.debug(f"Cloning git repo: {self.url}")
|
||||
try:
|
||||
|
||||
@@ -3,18 +3,13 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from core import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class CoreQuery:
|
||||
@strawberry.field
|
||||
def data_file(self, id: int) -> DataFileType:
|
||||
return models.DataFile.objects.get(pk=id)
|
||||
data_file: DataFileType = strawberry_django.field()
|
||||
data_file_list: List[DataFileType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def data_source(self, id: int) -> DataSourceType:
|
||||
return models.DataSource.objects.get(pk=id)
|
||||
data_source: DataSourceType = strawberry_django.field()
|
||||
data_source_list: List[DataSourceType] = strawberry_django.field()
|
||||
|
||||
@@ -19,6 +19,7 @@ REVISION_BUTTONS = """
|
||||
class ConfigRevisionTable(NetBoxTable):
|
||||
is_active = columns.BooleanColumn(
|
||||
verbose_name=_('Is Active'),
|
||||
false_mark=None
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('delete',),
|
||||
|
||||
@@ -25,12 +25,14 @@ from rq.registry import (
|
||||
from rq.worker import Worker
|
||||
from rq.worker_registration import clean_worker_registry
|
||||
|
||||
from extras.validators import CustomValidator
|
||||
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.htmx import htmx_partial
|
||||
from utilities.json import ConfigJSONEncoder
|
||||
from utilities.query import count_related
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
@@ -76,7 +78,10 @@ class DataSourceSyncView(BaseObjectView):
|
||||
datasource = get_object_or_404(self.queryset, pk=pk)
|
||||
job = datasource.enqueue_sync_job(request)
|
||||
|
||||
messages.success(request, f"Queued job #{job.pk} to sync {datasource}")
|
||||
messages.success(
|
||||
request,
|
||||
_("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource)
|
||||
)
|
||||
return redirect(datasource.get_absolute_url())
|
||||
|
||||
|
||||
@@ -235,7 +240,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
|
||||
|
||||
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
|
||||
candidate_config.activate()
|
||||
messages.success(request, f"Restored configuration revision #{pk}")
|
||||
messages.success(request, _("Restored configuration revision #{id}").format(id=pk))
|
||||
|
||||
return redirect(candidate_config.get_absolute_url())
|
||||
|
||||
@@ -379,9 +384,9 @@ class BackgroundTaskDeleteView(BaseRQView):
|
||||
# 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}')
|
||||
messages.success(request, _('Job {id} has been deleted.').format(id=job_id))
|
||||
else:
|
||||
messages.error(request, f'Error deleting job: {form.errors[0]}')
|
||||
messages.error(request, _('Error deleting job {id}: {error}').format(id=job_id, error=form.errors[0]))
|
||||
|
||||
return redirect(reverse('core:background_queue_list'))
|
||||
|
||||
@@ -394,13 +399,13 @@ class BackgroundTaskRequeueView(BaseRQView):
|
||||
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))
|
||||
raise Http404(_("Job {id} not found.").format(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}')
|
||||
messages.success(request, _('Job {id} has been re-enqueued.').format(id=job_id))
|
||||
return redirect(reverse('core:background_task', args=[job_id]))
|
||||
|
||||
|
||||
@@ -412,7 +417,7 @@ class BackgroundTaskEnqueueView(BaseRQView):
|
||||
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))
|
||||
raise Http404(_("Job {id} not found.").format(id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
@@ -435,7 +440,7 @@ class BackgroundTaskEnqueueView(BaseRQView):
|
||||
registry = ScheduledJobRegistry(queue.name, queue.connection)
|
||||
registry.remove(job)
|
||||
|
||||
messages.success(request, f'You have successfully enqueued: {job_id}')
|
||||
messages.success(request, _('Job {id} has been enqueued.').format(id=job_id))
|
||||
return redirect(reverse('core:background_task', args=[job_id]))
|
||||
|
||||
|
||||
@@ -452,11 +457,11 @@ class BackgroundTaskStopView(BaseRQView):
|
||||
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}')
|
||||
stopped_jobs = stop_jobs(queue, job_id)[0]
|
||||
if len(stopped_jobs) == 1:
|
||||
messages.success(request, _('Job {id} has been stopped.').format(id=job_id))
|
||||
else:
|
||||
messages.error(request, f'Failed to stop {job_id}')
|
||||
messages.error(request, _('Failed to stop job {id}').format(id=job_id))
|
||||
|
||||
return redirect(reverse('core:background_task', args=[job_id]))
|
||||
|
||||
@@ -559,22 +564,27 @@ class SystemView(UserPassesTestMixin, View):
|
||||
|
||||
# Raw data export
|
||||
if 'export' in request.GET:
|
||||
params = [param.name for param in PARAMS]
|
||||
data = {
|
||||
**stats,
|
||||
'plugins': {
|
||||
plugin.name: plugin.version for plugin in plugins
|
||||
},
|
||||
'config': {
|
||||
k: config.data[k] for k in sorted(config.data)
|
||||
k: getattr(config, k) for k in sorted(params)
|
||||
},
|
||||
}
|
||||
response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')
|
||||
response = HttpResponse(json.dumps(data, cls=ConfigJSONEncoder, indent=4), content_type='text/json')
|
||||
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
||||
return response
|
||||
|
||||
plugins_table = tables.PluginTable(plugins, orderable=False)
|
||||
plugins_table.configure(request)
|
||||
|
||||
# Serialize any CustomValidator classes
|
||||
if hasattr(config, 'CUSTOM_VALIDATORS') and config.CUSTOM_VALIDATORS:
|
||||
config.CUSTOM_VALIDATORS = json.dumps(config.CUSTOM_VALIDATORS, cls=ConfigJSONEncoder, indent=4)
|
||||
|
||||
return render(request, 'core/system.html', {
|
||||
'stats': stats,
|
||||
'plugins_table': plugins_table,
|
||||
|
||||
@@ -13,7 +13,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Legacy serializer for pre-v3.3 connections
|
||||
"""
|
||||
connected_endpoints_type = serializers.SerializerMethodField(read_only=True)
|
||||
connected_endpoints_type = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||
connected_endpoints = serializers.SerializerMethodField(read_only=True)
|
||||
connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
@@ -22,7 +22,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||
if endpoints := obj.connected_endpoints:
|
||||
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
|
||||
|
||||
@extend_schema_field(serializers.ListField)
|
||||
@extend_schema_field(serializers.ListField(allow_null=True))
|
||||
def get_connected_endpoints(self, obj):
|
||||
"""
|
||||
Return the appropriate serializer for the type of connected object.
|
||||
|
||||
@@ -91,7 +91,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
||||
class CabledObjectSerializer(serializers.ModelSerializer):
|
||||
cable = CableSerializer(nested=True, read_only=True, allow_null=True)
|
||||
cable_end = serializers.CharField(read_only=True)
|
||||
link_peers_type = serializers.SerializerMethodField(read_only=True)
|
||||
link_peers_type = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||
link_peers = serializers.SerializerMethodField(read_only=True)
|
||||
_occupied = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
@extend_schema_field(NestedDeviceSerializer)
|
||||
@extend_schema_field(NestedDeviceSerializer(allow_null=True))
|
||||
def get_parent_device(self, obj):
|
||||
try:
|
||||
device_bay = obj.parent_bay
|
||||
|
||||
@@ -886,6 +886,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_80211AD = 'ieee802.11ad'
|
||||
TYPE_80211AX = 'ieee802.11ax'
|
||||
TYPE_80211AY = 'ieee802.11ay'
|
||||
TYPE_80211BE = 'ieee802.11be'
|
||||
TYPE_802151 = 'ieee802.15.1'
|
||||
TYPE_OTHER_WIRELESS = 'other-wireless'
|
||||
|
||||
@@ -1057,6 +1058,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||
(TYPE_80211AX, 'IEEE 802.11ax'),
|
||||
(TYPE_80211AY, 'IEEE 802.11ay'),
|
||||
(TYPE_80211BE, 'IEEE 802.11be'),
|
||||
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
|
||||
(TYPE_OTHER_WIRELESS, 'Other (Wireless)'),
|
||||
)
|
||||
|
||||
@@ -49,6 +49,7 @@ WIRELESS_IFACE_TYPES = [
|
||||
InterfaceTypeChoices.TYPE_80211AD,
|
||||
InterfaceTypeChoices.TYPE_80211AX,
|
||||
InterfaceTypeChoices.TYPE_80211AY,
|
||||
InterfaceTypeChoices.TYPE_80211BE,
|
||||
InterfaceTypeChoices.TYPE_802151,
|
||||
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
|
||||
]
|
||||
|
||||
@@ -1389,12 +1389,12 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='model',
|
||||
label=_('Device type (model)'),
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
device_role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__role',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label=_('Device role (ID)'),
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
device_role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__role__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
@@ -1411,6 +1411,10 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label=_('Virtual Chassis'),
|
||||
)
|
||||
device_status = django_filters.MultipleChoiceFilter(
|
||||
choices=DeviceStatusChoices,
|
||||
field_name='device__status',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -1188,12 +1188,17 @@ class ComponentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, *args, initial=None, **kwargs):
|
||||
try:
|
||||
self.device_id = int(initial.get('device'))
|
||||
except (TypeError, ValueError):
|
||||
self.device_id = None
|
||||
|
||||
super().__init__(*args, initial=initial, **kwargs)
|
||||
|
||||
# Limit module queryset to Modules which belong to the parent Device
|
||||
if 'device' in self.initial:
|
||||
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||
if self.device_id:
|
||||
device = Device.objects.filter(pk=self.device_id).first()
|
||||
self.fields['module'].queryset = Module.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['module'].choices = ()
|
||||
@@ -1201,8 +1206,8 @@ class ComponentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
|
||||
class ConsolePortBulkEditForm(
|
||||
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1218,8 +1223,8 @@ class ConsolePortBulkEditForm(
|
||||
|
||||
|
||||
class ConsoleServerPortBulkEditForm(
|
||||
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1235,8 +1240,8 @@ class ConsoleServerPortBulkEditForm(
|
||||
|
||||
|
||||
class PowerPortBulkEditForm(
|
||||
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1253,8 +1258,8 @@ class PowerPortBulkEditForm(
|
||||
|
||||
|
||||
class PowerOutletBulkEditForm(
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1273,8 +1278,8 @@ class PowerOutletBulkEditForm(
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port queryset to PowerPorts which belong to the parent Device
|
||||
if 'device' in self.initial:
|
||||
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||
if self.device_id:
|
||||
device = Device.objects.filter(pk=self.device_id).first()
|
||||
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['power_port'].choices = ()
|
||||
@@ -1282,12 +1287,12 @@ class PowerOutletBulkEditForm(
|
||||
|
||||
|
||||
class InterfaceBulkEditForm(
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(Interface, [
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
|
||||
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
||||
'tx_power', 'wireless_lans'
|
||||
]),
|
||||
ComponentBulkEditForm
|
||||
])
|
||||
):
|
||||
enabled = forms.NullBooleanField(
|
||||
label=_('Enabled'),
|
||||
@@ -1416,8 +1421,8 @@ class InterfaceBulkEditForm(
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'device' in self.initial:
|
||||
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||
if self.device_id:
|
||||
device = Device.objects.filter(pk=self.device_id).first()
|
||||
|
||||
# Restrict parent/bridge/LAG interface assignment by device
|
||||
self.fields['parent'].widget.add_query_param('virtual_chassis_member_id', device.pk)
|
||||
@@ -1480,8 +1485,8 @@ class InterfaceBulkEditForm(
|
||||
|
||||
|
||||
class FrontPortBulkEditForm(
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1497,8 +1502,8 @@ class FrontPortBulkEditForm(
|
||||
|
||||
|
||||
class RearPortBulkEditForm(
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
|
||||
@@ -19,7 +19,7 @@ def get_cable_form(a_type, b_type):
|
||||
# Device component
|
||||
if hasattr(term_cls, 'device'):
|
||||
|
||||
attrs[f'termination_{cable_end}_device'] = DynamicModelChoiceField(
|
||||
attrs[f'termination_{cable_end}_device'] = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label=_('Device'),
|
||||
required=False,
|
||||
@@ -33,6 +33,7 @@ def get_cable_form(a_type, b_type):
|
||||
label=term_cls._meta.verbose_name.title(),
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
'parent': 'device',
|
||||
},
|
||||
query_params={
|
||||
'device_id': f'$termination_{cable_end}_device',
|
||||
@@ -43,7 +44,7 @@ def get_cable_form(a_type, b_type):
|
||||
# PowerFeed
|
||||
elif term_cls == PowerFeed:
|
||||
|
||||
attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelChoiceField(
|
||||
attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
label=_('Power Panel'),
|
||||
required=False,
|
||||
@@ -57,6 +58,7 @@ def get_cable_form(a_type, b_type):
|
||||
label=_('Power Feed'),
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
'parent': 'powerpanel',
|
||||
},
|
||||
query_params={
|
||||
'power_panel_id': f'$termination_{cable_end}_powerpanel',
|
||||
@@ -66,7 +68,7 @@ def get_cable_form(a_type, b_type):
|
||||
# CircuitTermination
|
||||
elif term_cls == CircuitTermination:
|
||||
|
||||
attrs[f'termination_{cable_end}_circuit'] = DynamicModelChoiceField(
|
||||
attrs[f'termination_{cable_end}_circuit'] = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
label=_('Circuit'),
|
||||
selector=True,
|
||||
@@ -79,6 +81,7 @@ def get_cable_form(a_type, b_type):
|
||||
label=_('Side'),
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
'parent': 'circuit',
|
||||
},
|
||||
query_params={
|
||||
'circuit_id': f'$termination_{cable_end}_circuit',
|
||||
|
||||
@@ -129,6 +129,11 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Device')
|
||||
)
|
||||
device_status = forms.MultipleChoiceField(
|
||||
choices=DeviceStatusChoices,
|
||||
required=False,
|
||||
label=_('Device Status'),
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
@@ -195,7 +200,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
|
||||
model = Location
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
@@ -232,6 +237,10 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
|
||||
choices=LocationStatusChoices,
|
||||
required=False
|
||||
)
|
||||
facility = forms.CharField(
|
||||
label=_('Facility'),
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@@ -1169,7 +1178,9 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1191,7 +1202,10 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1213,7 +1227,9 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1230,7 +1246,10 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1250,7 +1269,10 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', 'vdc_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'device_id')
|
||||
@@ -1358,7 +1380,9 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
)
|
||||
model = FrontPort
|
||||
@@ -1380,7 +1404,10 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1401,7 +1428,10 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
position = forms.CharField(
|
||||
@@ -1416,7 +1446,10 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1430,7 +1463,10 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
|
||||
@@ -3,208 +3,127 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from dcim import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class DCIMQuery:
|
||||
@strawberry.field
|
||||
def cable(self, id: int) -> CableType:
|
||||
return models.Cable.objects.get(pk=id)
|
||||
cable: CableType = strawberry_django.field()
|
||||
cable_list: List[CableType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_port(self, id: int) -> ConsolePortType:
|
||||
return models.ConsolePort.objects.get(pk=id)
|
||||
console_port: ConsolePortType = strawberry_django.field()
|
||||
console_port_list: List[ConsolePortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_port_template(self, id: int) -> ConsolePortTemplateType:
|
||||
return models.ConsolePortTemplate.objects.get(pk=id)
|
||||
console_port_template: ConsolePortTemplateType = strawberry_django.field()
|
||||
console_port_template_list: List[ConsolePortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_server_port(self, id: int) -> ConsoleServerPortType:
|
||||
return models.ConsoleServerPort.objects.get(pk=id)
|
||||
console_server_port: ConsoleServerPortType = strawberry_django.field()
|
||||
console_server_port_list: List[ConsoleServerPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_server_port_template(self, id: int) -> ConsoleServerPortTemplateType:
|
||||
return models.ConsoleServerPortTemplate.objects.get(pk=id)
|
||||
console_server_port_template: ConsoleServerPortTemplateType = strawberry_django.field()
|
||||
console_server_port_template_list: List[ConsoleServerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device(self, id: int) -> DeviceType:
|
||||
return models.Device.objects.get(pk=id)
|
||||
device: DeviceType = strawberry_django.field()
|
||||
device_list: List[DeviceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_bay(self, id: int) -> DeviceBayType:
|
||||
return models.DeviceBay.objects.get(pk=id)
|
||||
device_bay: DeviceBayType = strawberry_django.field()
|
||||
device_bay_list: List[DeviceBayType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_bay_template(self, id: int) -> DeviceBayTemplateType:
|
||||
return models.DeviceBayTemplate.objects.get(pk=id)
|
||||
device_bay_template: DeviceBayTemplateType = strawberry_django.field()
|
||||
device_bay_template_list: List[DeviceBayTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_role(self, id: int) -> DeviceRoleType:
|
||||
return models.DeviceRole.objects.get(pk=id)
|
||||
device_role: DeviceRoleType = strawberry_django.field()
|
||||
device_role_list: List[DeviceRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_type(self, id: int) -> DeviceTypeType:
|
||||
return models.DeviceType.objects.get(pk=id)
|
||||
device_type: DeviceTypeType = strawberry_django.field()
|
||||
device_type_list: List[DeviceTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def front_port(self, id: int) -> FrontPortType:
|
||||
return models.FrontPort.objects.get(pk=id)
|
||||
front_port: FrontPortType = strawberry_django.field()
|
||||
front_port_list: List[FrontPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def front_port_template(self, id: int) -> FrontPortTemplateType:
|
||||
return models.FrontPortTemplate.objects.get(pk=id)
|
||||
front_port_template: FrontPortTemplateType = strawberry_django.field()
|
||||
front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def interface(self, id: int) -> InterfaceType:
|
||||
return models.Interface.objects.get(pk=id)
|
||||
interface: InterfaceType = strawberry_django.field()
|
||||
interface_list: List[InterfaceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def interface_template(self, id: int) -> InterfaceTemplateType:
|
||||
return models.InterfaceTemplate.objects.get(pk=id)
|
||||
interface_template: InterfaceTemplateType = strawberry_django.field()
|
||||
interface_template_list: List[InterfaceTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item(self, id: int) -> InventoryItemType:
|
||||
return models.InventoryItem.objects.get(pk=id)
|
||||
inventory_item: InventoryItemType = strawberry_django.field()
|
||||
inventory_item_list: List[InventoryItemType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item_role(self, id: int) -> InventoryItemRoleType:
|
||||
return models.InventoryItemRole.objects.get(pk=id)
|
||||
inventory_item_role: InventoryItemRoleType = strawberry_django.field()
|
||||
inventory_item_role_list: List[InventoryItemRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item_template(self, id: int) -> InventoryItemTemplateType:
|
||||
return models.InventoryItemTemplate.objects.get(pk=id)
|
||||
inventory_item_template: InventoryItemTemplateType = strawberry_django.field()
|
||||
inventory_item_template_list: List[InventoryItemTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def location(self, id: int) -> LocationType:
|
||||
return models.Location.objects.get(pk=id)
|
||||
location: LocationType = strawberry_django.field()
|
||||
location_list: List[LocationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def manufacturer(self, id: int) -> ManufacturerType:
|
||||
return models.Manufacturer.objects.get(pk=id)
|
||||
manufacturer: ManufacturerType = strawberry_django.field()
|
||||
manufacturer_list: List[ManufacturerType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module(self, id: int) -> ModuleType:
|
||||
return models.Module.objects.get(pk=id)
|
||||
module: ModuleType = strawberry_django.field()
|
||||
module_list: List[ModuleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_bay(self, id: int) -> ModuleBayType:
|
||||
return models.ModuleBay.objects.get(pk=id)
|
||||
module_bay: ModuleBayType = strawberry_django.field()
|
||||
module_bay_list: List[ModuleBayType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_bay_template(self, id: int) -> ModuleBayTemplateType:
|
||||
return models.ModuleBayTemplate.objects.get(pk=id)
|
||||
module_bay_template: ModuleBayTemplateType = strawberry_django.field()
|
||||
module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_type(self, id: int) -> ModuleTypeType:
|
||||
return models.ModuleType.objects.get(pk=id)
|
||||
module_type: ModuleTypeType = strawberry_django.field()
|
||||
module_type_list: List[ModuleTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def platform(self, id: int) -> PlatformType:
|
||||
return models.Platform.objects.get(pk=id)
|
||||
platform: PlatformType = strawberry_django.field()
|
||||
platform_list: List[PlatformType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_feed(self, id: int) -> PowerFeedType:
|
||||
return models.PowerFeed.objects.get(pk=id)
|
||||
power_feed: PowerFeedType = strawberry_django.field()
|
||||
power_feed_list: List[PowerFeedType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_outlet(self, id: int) -> PowerOutletType:
|
||||
return models.PowerOutlet.objects.get(pk=id)
|
||||
power_outlet: PowerOutletType = strawberry_django.field()
|
||||
power_outlet_list: List[PowerOutletType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_outlet_template(self, id: int) -> PowerOutletTemplateType:
|
||||
return models.PowerOutletTemplate.objects.get(pk=id)
|
||||
power_outlet_template: PowerOutletTemplateType = strawberry_django.field()
|
||||
power_outlet_template_list: List[PowerOutletTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_panel(self, id: int) -> PowerPanelType:
|
||||
return models.PowerPanel.objects.get(id=id)
|
||||
power_panel: PowerPanelType = strawberry_django.field()
|
||||
power_panel_list: List[PowerPanelType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_port(self, id: int) -> PowerPortType:
|
||||
return models.PowerPort.objects.get(id=id)
|
||||
power_port: PowerPortType = strawberry_django.field()
|
||||
power_port_list: List[PowerPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_port_template(self, id: int) -> PowerPortTemplateType:
|
||||
return models.PowerPortTemplate.objects.get(id=id)
|
||||
power_port_template: PowerPortTemplateType = strawberry_django.field()
|
||||
power_port_template_list: List[PowerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack(self, id: int) -> RackType:
|
||||
return models.Rack.objects.get(id=id)
|
||||
rack: RackType = strawberry_django.field()
|
||||
rack_list: List[RackType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_reservation(self, id: int) -> RackReservationType:
|
||||
return models.RackReservation.objects.get(id=id)
|
||||
rack_reservation: RackReservationType = strawberry_django.field()
|
||||
rack_reservation_list: List[RackReservationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_role(self, id: int) -> RackRoleType:
|
||||
return models.RackRole.objects.get(id=id)
|
||||
rack_role: RackRoleType = strawberry_django.field()
|
||||
rack_role_list: List[RackRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rear_port(self, id: int) -> RearPortType:
|
||||
return models.RearPort.objects.get(id=id)
|
||||
rear_port: RearPortType = strawberry_django.field()
|
||||
rear_port_list: List[RearPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rear_port_template(self, id: int) -> RearPortTemplateType:
|
||||
return models.RearPortTemplate.objects.get(id=id)
|
||||
rear_port_template: RearPortTemplateType = strawberry_django.field()
|
||||
rear_port_template_list: List[RearPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def region(self, id: int) -> RegionType:
|
||||
return models.Region.objects.get(id=id)
|
||||
region: RegionType = strawberry_django.field()
|
||||
region_list: List[RegionType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def site(self, id: int) -> SiteType:
|
||||
return models.Site.objects.get(id=id)
|
||||
site: SiteType = strawberry_django.field()
|
||||
site_list: List[SiteType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def site_group(self, id: int) -> SiteGroupType:
|
||||
return models.SiteGroup.objects.get(id=id)
|
||||
site_group: SiteGroupType = strawberry_django.field()
|
||||
site_group_list: List[SiteGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_chassis(self, id: int) -> VirtualChassisType:
|
||||
return models.VirtualChassis.objects.get(id=id)
|
||||
virtual_chassis: VirtualChassisType = strawberry_django.field()
|
||||
virtual_chassis_list: List[VirtualChassisType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_device_context(self, id: int) -> VirtualDeviceContextType:
|
||||
return models.VirtualDeviceContext.objects.get(id=id)
|
||||
virtual_device_context: VirtualDeviceContextType = strawberry_django.field()
|
||||
virtual_device_context_list: List[VirtualDeviceContextType] = strawberry_django.field()
|
||||
|
||||
@@ -88,6 +88,8 @@ class Cable(PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
clone_fields = ('tenant', 'type',)
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
verbose_name = _('cable')
|
||||
|
||||
@@ -63,7 +63,10 @@ class DeviceRoleTable(NetBoxTable):
|
||||
verbose_name=_('VMs')
|
||||
)
|
||||
color = columns.ColorColumn()
|
||||
vm_role = columns.BooleanColumn()
|
||||
vm_role = columns.BooleanColumn(
|
||||
verbose_name=_('VM role'),
|
||||
false_mark=None
|
||||
)
|
||||
config_template = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -287,6 +290,11 @@ class DeviceComponentTable(NetBoxTable):
|
||||
linkify=True,
|
||||
order_by=('_name',)
|
||||
)
|
||||
device_status = columns.ChoiceFieldColumn(
|
||||
accessor=tables.A('device__status'),
|
||||
verbose_name=_('Device Status'),
|
||||
color=lambda x: x.device.get_status_color(),
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
order_by = ('device', 'name')
|
||||
@@ -329,6 +337,7 @@ class CableTerminationTable(NetBoxTable):
|
||||
)
|
||||
mark_connected = columns.BooleanColumn(
|
||||
verbose_name=_('Mark Connected'),
|
||||
false_mark=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -586,7 +595,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
||||
}
|
||||
)
|
||||
mgmt_only = columns.BooleanColumn(
|
||||
verbose_name=_('Management Only')
|
||||
verbose_name=_('Management Only'),
|
||||
false_mark=None
|
||||
)
|
||||
speed_formatted = columns.TemplateColumn(
|
||||
template_code='{% load helpers %}{{ value|humanize_speed }}',
|
||||
@@ -913,6 +923,7 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
)
|
||||
discovered = columns.BooleanColumn(
|
||||
verbose_name=_('Discovered'),
|
||||
false_mark=None
|
||||
)
|
||||
parent = tables.Column(
|
||||
linkify=True,
|
||||
|
||||
@@ -86,7 +86,8 @@ class DeviceTypeTable(NetBoxTable):
|
||||
linkify=True
|
||||
)
|
||||
is_full_depth = columns.BooleanColumn(
|
||||
verbose_name=_('Full Depth')
|
||||
verbose_name=_('Full Depth'),
|
||||
false_mark=None
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
@@ -98,7 +99,10 @@ class DeviceTypeTable(NetBoxTable):
|
||||
verbose_name=_('U Height'),
|
||||
template_code='{{ value|floatformat }}'
|
||||
)
|
||||
exclude_from_utilization = columns.BooleanColumn()
|
||||
exclude_from_utilization = columns.BooleanColumn(
|
||||
verbose_name=_('Exclude from utilization'),
|
||||
false_mark=None
|
||||
)
|
||||
weight = columns.TemplateColumn(
|
||||
verbose_name=_('Weight'),
|
||||
template_code=WEIGHT,
|
||||
@@ -221,7 +225,8 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
verbose_name=_('Enabled'),
|
||||
)
|
||||
mgmt_only = columns.BooleanColumn(
|
||||
verbose_name=_('Management Only')
|
||||
verbose_name=_('Management Only'),
|
||||
false_mark=None
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('edit', 'delete'),
|
||||
|
||||
@@ -99,6 +99,11 @@ class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
url_params={'site_id': 'pk'},
|
||||
verbose_name=_('ASN Count')
|
||||
)
|
||||
device_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:device_list',
|
||||
url_params={'site_id': 'pk'},
|
||||
verbose_name=_('Devices')
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
)
|
||||
|
||||
@@ -32,13 +32,19 @@ class DeviceComponentFilterSetTests:
|
||||
params = {'device_type': [device_types[0].model, device_types[1].model]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_role(self):
|
||||
def test_device_role(self):
|
||||
role = DeviceRole.objects.all()[:2]
|
||||
params = {'role_id': [role[0].pk, role[1].pk]}
|
||||
params = {'device_role_id': [role[0].pk, role[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'role': [role[0].slug, role[1].slug]}
|
||||
params = {'device_role': [role[0].slug, role[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_device_status(self):
|
||||
params = {'device_status': ['active']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'device_status': ['offline', 'active']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class DeviceComponentTemplateFilterSetTests:
|
||||
|
||||
@@ -2588,10 +2594,10 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[0], role=roles[0], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='active'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[0], role=roles[0], site=sites[3], status='planned'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -2768,10 +2774,10 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='active'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='planned'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -2948,10 +2954,10 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='active'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='planned'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -3136,10 +3142,10 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='active'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='planned'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -3334,7 +3340,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
rack=racks[0],
|
||||
virtual_chassis=virtual_chassis,
|
||||
vc_position=1,
|
||||
vc_priority=1
|
||||
vc_priority=1,
|
||||
status='active',
|
||||
),
|
||||
Device(
|
||||
name='Device 1B',
|
||||
@@ -3345,7 +3352,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
rack=racks[2],
|
||||
virtual_chassis=virtual_chassis,
|
||||
vc_position=2,
|
||||
vc_priority=1
|
||||
vc_priority=1,
|
||||
status='active',
|
||||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
@@ -3353,7 +3361,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
location=locations[1],
|
||||
rack=racks[1]
|
||||
rack=racks[1],
|
||||
status='offline',
|
||||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
@@ -3361,14 +3370,16 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
location=locations[2],
|
||||
rack=racks[2]
|
||||
rack=racks[2],
|
||||
status='planned',
|
||||
),
|
||||
# For cable connections
|
||||
Device(
|
||||
name=None,
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[3]
|
||||
site=sites[3],
|
||||
status='planned',
|
||||
),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@@ -3814,10 +3825,10 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='active'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='planned'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -4003,10 +4014,10 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='active'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='planned'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -4184,9 +4195,9 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='active'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -4313,9 +4324,9 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='active'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -4547,6 +4558,13 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'device_type': [device_types[0].model, device_types[1].model]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_device_role(self):
|
||||
role = DeviceRole.objects.all()[:2]
|
||||
params = {'device_role_id': [role[0].pk, role[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'device_role': [role[0].slug, role[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_role(self):
|
||||
role = DeviceRole.objects.all()[:2]
|
||||
params = {'role_id': [role[0].pk, role[1].pk]}
|
||||
|
||||
@@ -380,7 +380,9 @@ class SiteGroupContactsView(ObjectContactsView):
|
||||
#
|
||||
|
||||
class SiteListView(generic.ObjectListView):
|
||||
queryset = Site.objects.all()
|
||||
queryset = Site.objects.annotate(
|
||||
device_count=count_related(Device, 'site')
|
||||
)
|
||||
filterset = filtersets.SiteFilterSet
|
||||
filterset_form = forms.SiteFilterForm
|
||||
table = tables.SiteTable
|
||||
@@ -2059,7 +2061,7 @@ class DeviceRenderConfigView(generic.ObjectView):
|
||||
try:
|
||||
rendered_config = config_template.render(context=context_data)
|
||||
except TemplateError as e:
|
||||
messages.error(request, f"An error occurred while rendering the template: {e}")
|
||||
messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e))
|
||||
rendered_config = traceback.format_exc()
|
||||
|
||||
return {
|
||||
@@ -2823,7 +2825,13 @@ class DeviceBayPopulateView(generic.ObjectEditView):
|
||||
device_bay.snapshot()
|
||||
device_bay.installed_device = form.cleaned_data['installed_device']
|
||||
device_bay.save()
|
||||
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
|
||||
messages.success(
|
||||
request,
|
||||
_("Installed device {device} in bay {device_bay}.").format(
|
||||
device=device_bay.installed_device,
|
||||
device_bay=device_bay
|
||||
)
|
||||
)
|
||||
return_url = self.get_return_url(request)
|
||||
|
||||
return redirect(return_url)
|
||||
@@ -2858,7 +2866,13 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
|
||||
removed_device = device_bay.installed_device
|
||||
device_bay.installed_device = None
|
||||
device_bay.save()
|
||||
messages.success(request, f"{removed_device} has been removed from {device_bay}.")
|
||||
messages.success(
|
||||
request,
|
||||
_("Removed device {device} from bay {device_bay}.").format(
|
||||
device=removed_device,
|
||||
device_bay=device_bay
|
||||
)
|
||||
)
|
||||
return_url = self.get_return_url(request, device_bay.device)
|
||||
|
||||
return redirect(return_url)
|
||||
@@ -3426,7 +3440,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
|
||||
|
||||
membership_form.save()
|
||||
messages.success(request, mark_safe(
|
||||
f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
|
||||
_('Added member <a href="{url}">{device}</a>').format(url=device.get_absolute_url(), device=escape(device))
|
||||
))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
@@ -3471,7 +3485,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
|
||||
# Protect master device from being removed
|
||||
virtual_chassis = VirtualChassis.objects.filter(master=device).first()
|
||||
if virtual_chassis is not None:
|
||||
messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
|
||||
messages.error(
|
||||
request,
|
||||
_('Unable to remove master device {device} from the virtual chassis.').format(device=device)
|
||||
)
|
||||
return redirect(device.get_absolute_url())
|
||||
|
||||
if form.is_valid():
|
||||
@@ -3483,7 +3500,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
|
||||
device.vc_priority = None
|
||||
device.save()
|
||||
|
||||
msg = 'Removed {} from virtual chassis {}'.format(device, device.virtual_chassis)
|
||||
msg = _('Removed {device} from virtual chassis {chassis}').format(
|
||||
device=device,
|
||||
chassis=device.virtual_chassis
|
||||
)
|
||||
messages.success(request, msg)
|
||||
|
||||
return redirect(self.get_return_url(request, device))
|
||||
|
||||
@@ -19,6 +19,8 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
queryset=ObjectType.objects.all()
|
||||
)
|
||||
parent = serializers.SerializerMethodField(read_only=True)
|
||||
image_width = serializers.IntegerField(read_only=True)
|
||||
image_height = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
|
||||
@@ -39,7 +39,7 @@ class ScriptSerializer(ValidatedModelSerializer):
|
||||
def get_display(self, obj):
|
||||
return f'{obj.name} ({obj.module})'
|
||||
|
||||
@extend_schema_field(serializers.CharField())
|
||||
@extend_schema_field(serializers.CharField(allow_null=True))
|
||||
def get_description(self, obj):
|
||||
if obj.python_class:
|
||||
return obj.python_class().description
|
||||
|
||||
@@ -131,22 +131,6 @@ class DashboardWidget:
|
||||
def name(self):
|
||||
return f'{self.__class__.__module__.split(".")[0]}.{self.__class__.__name__}'
|
||||
|
||||
@property
|
||||
def fg_color(self):
|
||||
"""
|
||||
Return the appropriate foreground (text) color for the widget's color.
|
||||
"""
|
||||
if self.color in (
|
||||
ButtonColorChoices.CYAN,
|
||||
ButtonColorChoices.GRAY,
|
||||
ButtonColorChoices.GREY,
|
||||
ButtonColorChoices.TEAL,
|
||||
ButtonColorChoices.WHITE,
|
||||
ButtonColorChoices.YELLOW,
|
||||
):
|
||||
return ButtonColorChoices.BLACK
|
||||
return ButtonColorChoices.WHITE
|
||||
|
||||
@property
|
||||
def form_data(self):
|
||||
return {
|
||||
@@ -251,6 +235,10 @@ class ObjectListWidget(DashboardWidget):
|
||||
def render(self, request):
|
||||
app_label, model_name = self.config['model'].split('.')
|
||||
model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class()
|
||||
if not model:
|
||||
logger.debug(f"Dashboard Widget model_class not found: {app_label}:{model_name}")
|
||||
return
|
||||
|
||||
viewname = get_viewname(model, action='list')
|
||||
|
||||
# Evaluate user's permission. Note that this controls only whether the HTMX element is
|
||||
|
||||
@@ -31,7 +31,7 @@ class ReportForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Annotate the current system time for reference
|
||||
now = local_now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
now = local_now().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
self.fields['schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
|
||||
|
||||
# Remove scheduling fields if scheduling is disabled
|
||||
|
||||
@@ -37,7 +37,7 @@ class ScriptForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Annotate the current system time for reference
|
||||
now = local_now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
now = local_now().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
self.fields['_schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
|
||||
|
||||
# Remove scheduling fields if scheduling is disabled
|
||||
|
||||
@@ -3,68 +3,43 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from extras import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class ExtrasQuery:
|
||||
@strawberry.field
|
||||
def config_context(self, id: int) -> ConfigContextType:
|
||||
return models.ConfigContext.objects.get(pk=id)
|
||||
config_context: ConfigContextType = strawberry_django.field()
|
||||
config_context_list: List[ConfigContextType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def config_template(self, id: int) -> ConfigTemplateType:
|
||||
return models.ConfigTemplate.objects.get(pk=id)
|
||||
config_template: ConfigTemplateType = strawberry_django.field()
|
||||
config_template_list: List[ConfigTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def custom_field(self, id: int) -> CustomFieldType:
|
||||
return models.CustomField.objects.get(pk=id)
|
||||
custom_field: CustomFieldType = strawberry_django.field()
|
||||
custom_field_list: List[CustomFieldType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def custom_field_choice_set(self, id: int) -> CustomFieldChoiceSetType:
|
||||
return models.CustomFieldChoiceSet.objects.get(pk=id)
|
||||
custom_field_choice_set: CustomFieldChoiceSetType = strawberry_django.field()
|
||||
custom_field_choice_set_list: List[CustomFieldChoiceSetType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def custom_link(self, id: int) -> CustomLinkType:
|
||||
return models.CustomLink.objects.get(pk=id)
|
||||
custom_link: CustomLinkType = strawberry_django.field()
|
||||
custom_link_list: List[CustomLinkType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def export_template(self, id: int) -> ExportTemplateType:
|
||||
return models.ExportTemplate.objects.get(pk=id)
|
||||
export_template: ExportTemplateType = strawberry_django.field()
|
||||
export_template_list: List[ExportTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def image_attachment(self, id: int) -> ImageAttachmentType:
|
||||
return models.ImageAttachment.objects.get(pk=id)
|
||||
image_attachment: ImageAttachmentType = strawberry_django.field()
|
||||
image_attachment_list: List[ImageAttachmentType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def saved_filter(self, id: int) -> SavedFilterType:
|
||||
return models.SavedFilter.objects.get(pk=id)
|
||||
saved_filter: SavedFilterType = strawberry_django.field()
|
||||
saved_filter_list: List[SavedFilterType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def journal_entry(self, id: int) -> JournalEntryType:
|
||||
return models.JournalEntry.objects.get(pk=id)
|
||||
journal_entry: JournalEntryType = strawberry_django.field()
|
||||
journal_entry_list: List[JournalEntryType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def tag(self, id: int) -> TagType:
|
||||
return models.Tag.objects.get(pk=id)
|
||||
tag: TagType = strawberry_django.field()
|
||||
tag_list: List[TagType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def webhook(self, id: int) -> WebhookType:
|
||||
return models.Webhook.objects.get(pk=id)
|
||||
webhook: WebhookType = strawberry_django.field()
|
||||
webhook_list: List[WebhookType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def event_rule(self, id: int) -> EventRuleType:
|
||||
return models.EventRule.objects.get(pk=id)
|
||||
event_rule: EventRuleType = strawberry_django.field()
|
||||
event_rule_list: List[EventRuleType] = strawberry_django.field()
|
||||
|
||||
25
netbox/extras/migrations/0116_custom_link_button_color.py
Normal file
25
netbox/extras/migrations/0116_custom_link_button_color.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def update_link_buttons(apps, schema_editor):
|
||||
CustomLink = apps.get_model('extras', 'CustomLink')
|
||||
CustomLink.objects.filter(button_class='outline-dark').update(button_class='default')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0115_convert_dashboard_widgets'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customlink',
|
||||
name='button_class',
|
||||
field=models.CharField(default='default', max_length=30),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=update_link_buttons,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@@ -352,13 +352,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
|
||||
if not self.related_object_type:
|
||||
raise ValidationError({
|
||||
'object_type': _("Object fields must define an object type.")
|
||||
'related_object_type': _("Object fields must define an object type.")
|
||||
})
|
||||
elif self.related_object_type:
|
||||
raise ValidationError({
|
||||
'object_type': _(
|
||||
"{type} fields may not define an object type.")
|
||||
.format(type=self.get_type_display())
|
||||
'type': _("{type} fields may not define an object type.") .format(type=self.get_type_display())
|
||||
})
|
||||
|
||||
def serialize(self, value):
|
||||
|
||||
@@ -47,7 +47,8 @@ class CustomFieldTable(NetBoxTable):
|
||||
verbose_name=_('Object Types')
|
||||
)
|
||||
required = columns.BooleanColumn(
|
||||
verbose_name=_('Required')
|
||||
verbose_name=_('Required'),
|
||||
false_mark=None
|
||||
)
|
||||
ui_visible = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Visible')
|
||||
@@ -72,6 +73,7 @@ class CustomFieldTable(NetBoxTable):
|
||||
)
|
||||
is_cloneable = columns.BooleanColumn(
|
||||
verbose_name=_('Is Cloneable'),
|
||||
false_mark=None
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
@@ -105,6 +107,7 @@ class CustomFieldChoiceSetTable(NetBoxTable):
|
||||
)
|
||||
order_alphabetically = columns.BooleanColumn(
|
||||
verbose_name=_('Order Alphabetically'),
|
||||
false_mark=None
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
@@ -129,6 +132,7 @@ class CustomLinkTable(NetBoxTable):
|
||||
)
|
||||
new_window = columns.BooleanColumn(
|
||||
verbose_name=_('New Window'),
|
||||
false_mark=None
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
@@ -150,6 +154,7 @@ class ExportTemplateTable(NetBoxTable):
|
||||
)
|
||||
as_attachment = columns.BooleanColumn(
|
||||
verbose_name=_('As Attachment'),
|
||||
false_mark=None
|
||||
)
|
||||
data_source = tables.Column(
|
||||
verbose_name=_('Data Source'),
|
||||
@@ -218,6 +223,7 @@ class SavedFilterTable(NetBoxTable):
|
||||
)
|
||||
shared = columns.BooleanColumn(
|
||||
verbose_name=_('Shared'),
|
||||
false_mark=None
|
||||
)
|
||||
|
||||
def value_parameters(self, value):
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.utils.safestring import mark_safe
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.models import CustomLink
|
||||
from netbox.choices import ButtonColorChoices
|
||||
|
||||
|
||||
register = template.Library()
|
||||
@@ -59,10 +60,11 @@ def custom_links(context, obj):
|
||||
|
||||
# Add non-grouped links
|
||||
else:
|
||||
button_class = 'outline-secondary' if cl.button_class == ButtonColorChoices.DEFAULT else cl.button_class
|
||||
try:
|
||||
if rendered := cl.render(link_context):
|
||||
template_code += LINK_BUTTON.format(
|
||||
rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
|
||||
rendered['link'], rendered['link_target'], button_class, rendered['text']
|
||||
)
|
||||
except Exception as e:
|
||||
template_code += f'<a class="btn btn-sm btn-outline-secondary" disabled="disabled" title="{e}">' \
|
||||
|
||||
@@ -1258,6 +1258,9 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
||||
|
||||
# If this is an HTMX request, return only the result HTML
|
||||
if htmx_partial(request):
|
||||
if request.GET.get('log'):
|
||||
# If log=True, render only the log table
|
||||
return render(request, 'htmx/table.html', context)
|
||||
response = render(request, 'extras/htmx/script_result.html', context)
|
||||
if job.completed or not job.started:
|
||||
response.status_code = 286
|
||||
|
||||
@@ -221,6 +221,19 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('VLAN Group')
|
||||
)
|
||||
vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label=_('VLAN'),
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
}
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@@ -269,9 +282,10 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
|
||||
FieldSet('tenant', 'status', 'role', 'description'),
|
||||
FieldSet('region', 'site_group', 'site', name=_('Site')),
|
||||
FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
|
||||
FieldSet('vlan_group', 'vlan', name=_('VLAN Assignment')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'site', 'vrf', 'tenant', 'role', 'description', 'comments',
|
||||
'site', 'vlan', 'vrf', 'tenant', 'role', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
|
||||
from dcim.models import Device, Interface, Site
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
from ipam.formfields import IPNetworkFormField
|
||||
@@ -17,8 +17,10 @@ from utilities.forms.fields import (
|
||||
SlugField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
|
||||
from utilities.forms.widgets import DatePicker
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
|
||||
from utilities.forms.utils import get_field_value
|
||||
from utilities.forms.widgets import DatePicker, HTMXSelect
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
|
||||
__all__ = (
|
||||
'AggregateForm',
|
||||
@@ -562,91 +564,31 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
|
||||
|
||||
|
||||
class VLANGroupForm(NetBoxModelForm):
|
||||
scope_type = ContentTypeChoiceField(
|
||||
label=_('Scope type'),
|
||||
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
||||
required=False
|
||||
)
|
||||
region = DynamicModelChoiceField(
|
||||
label=_('Region'),
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
sitegroup = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
},
|
||||
label=_('Site group')
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'locations': '$location'
|
||||
},
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$sitegroup',
|
||||
}
|
||||
)
|
||||
location = DynamicModelChoiceField(
|
||||
label=_('Location'),
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'racks': '$rack'
|
||||
},
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
label=_('Rack'),
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
}
|
||||
)
|
||||
clustergroup = DynamicModelChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'clusters': '$cluster'
|
||||
},
|
||||
label=_('Cluster group')
|
||||
)
|
||||
cluster = DynamicModelChoiceField(
|
||||
label=_('Cluster'),
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$clustergroup',
|
||||
}
|
||||
)
|
||||
slug = SlugField()
|
||||
scope_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
label=_('Scope type')
|
||||
)
|
||||
scope = DynamicModelChoiceField(
|
||||
label=_('Scope'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
|
||||
FieldSet('min_vid', 'max_vid', name=_('Child VLANs')),
|
||||
FieldSet(
|
||||
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
|
||||
name=_('Scope')
|
||||
),
|
||||
FieldSet('scope_type', 'scope', name=_('Scope')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
||||
'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags',
|
||||
'name', 'slug', 'description', 'min_vid', 'max_vid', 'scope_type', 'scope', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -654,21 +596,30 @@ class VLANGroupForm(NetBoxModelForm):
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.scope:
|
||||
initial[instance.scope_type.model] = instance.scope
|
||||
|
||||
initial['scope'] = instance.scope
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if scope_type_id := get_field_value(self, 'scope_type'):
|
||||
try:
|
||||
scope_type = ContentType.objects.get(pk=scope_type_id)
|
||||
model = scope_type.model_class()
|
||||
self.fields['scope'].queryset = model.objects.all()
|
||||
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['scope'].disabled = False
|
||||
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
if self.instance and scope_type_id != self.instance.scope_type_id:
|
||||
self.initial['scope'] = None
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign scope based on scope_type
|
||||
if self.cleaned_data.get('scope_type'):
|
||||
scope_field = self.cleaned_data['scope_type'].model
|
||||
self.instance.scope = self.cleaned_data.get(scope_field)
|
||||
else:
|
||||
self.instance.scope_id = None
|
||||
# Assign the selected scope (if any)
|
||||
self.instance.scope = self.cleaned_data.get('scope')
|
||||
|
||||
|
||||
class VLANForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
@@ -3,88 +3,55 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from ipam import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class IPAMQuery:
|
||||
@strawberry.field
|
||||
def asn(self, id: int) -> ASNType:
|
||||
return models.ASN.objects.get(pk=id)
|
||||
asn: ASNType = strawberry_django.field()
|
||||
asn_list: List[ASNType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def asn_range(self, id: int) -> ASNRangeType:
|
||||
return models.ASNRange.objects.get(pk=id)
|
||||
asn_range: ASNRangeType = strawberry_django.field()
|
||||
asn_range_list: List[ASNRangeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def aggregate(self, id: int) -> AggregateType:
|
||||
return models.Aggregate.objects.get(pk=id)
|
||||
aggregate: AggregateType = strawberry_django.field()
|
||||
aggregate_list: List[AggregateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def ip_address(self, id: int) -> IPAddressType:
|
||||
return models.IPAddress.objects.get(pk=id)
|
||||
ip_address: IPAddressType = strawberry_django.field()
|
||||
ip_address_list: List[IPAddressType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def ip_range(self, id: int) -> IPRangeType:
|
||||
return models.IPRange.objects.get(pk=id)
|
||||
ip_range: IPRangeType = strawberry_django.field()
|
||||
ip_range_list: List[IPRangeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def prefix(self, id: int) -> PrefixType:
|
||||
return models.Prefix.objects.get(pk=id)
|
||||
prefix: PrefixType = strawberry_django.field()
|
||||
prefix_list: List[PrefixType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rir(self, id: int) -> RIRType:
|
||||
return models.RIR.objects.get(pk=id)
|
||||
rir: RIRType = strawberry_django.field()
|
||||
rir_list: List[RIRType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def role(self, id: int) -> RoleType:
|
||||
return models.Role.objects.get(pk=id)
|
||||
role: RoleType = strawberry_django.field()
|
||||
role_list: List[RoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def route_target(self, id: int) -> RouteTargetType:
|
||||
return models.RouteTarget.objects.get(pk=id)
|
||||
route_target: RouteTargetType = strawberry_django.field()
|
||||
route_target_list: List[RouteTargetType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def service(self, id: int) -> ServiceType:
|
||||
return models.Service.objects.get(pk=id)
|
||||
service: ServiceType = strawberry_django.field()
|
||||
service_list: List[ServiceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def service_template(self, id: int) -> ServiceTemplateType:
|
||||
return models.ServiceTemplate.objects.get(pk=id)
|
||||
service_template: ServiceTemplateType = strawberry_django.field()
|
||||
service_template_list: List[ServiceTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def fhrp_group(self, id: int) -> FHRPGroupType:
|
||||
return models.FHRPGroup.objects.get(pk=id)
|
||||
fhrp_group: FHRPGroupType = strawberry_django.field()
|
||||
fhrp_group_list: List[FHRPGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def fhrp_group_assignment(self, id: int) -> FHRPGroupAssignmentType:
|
||||
return models.FHRPGroupAssignment.objects.get(pk=id)
|
||||
fhrp_group_assignment: FHRPGroupAssignmentType = strawberry_django.field()
|
||||
fhrp_group_assignment_list: List[FHRPGroupAssignmentType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def vlan(self, id: int) -> VLANType:
|
||||
return models.VLAN.objects.get(pk=id)
|
||||
vlan: VLANType = strawberry_django.field()
|
||||
vlan_list: List[VLANType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def vlan_group(self, id: int) -> VLANGroupType:
|
||||
return models.VLANGroup.objects.get(pk=id)
|
||||
vlan_group: VLANGroupType = strawberry_django.field()
|
||||
vlan_group_list: List[VLANGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def vrf(self, id: int) -> VRFType:
|
||||
return models.VRF.objects.get(pk=id)
|
||||
vrf: VRFType = strawberry_django.field()
|
||||
vrf_list: List[VRFType] = strawberry_django.field()
|
||||
|
||||
@@ -86,7 +86,8 @@ class RIRTable(NetBoxTable):
|
||||
linkify=True
|
||||
)
|
||||
is_private = columns.BooleanColumn(
|
||||
verbose_name=_('Private')
|
||||
verbose_name=_('Private'),
|
||||
false_mark=None
|
||||
)
|
||||
aggregate_count = columns.LinkedCountColumn(
|
||||
viewname='ipam:aggregate_list',
|
||||
@@ -258,10 +259,12 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
|
||||
linkify=True
|
||||
)
|
||||
is_pool = columns.BooleanColumn(
|
||||
verbose_name=_('Pool')
|
||||
verbose_name=_('Pool'),
|
||||
false_mark=None
|
||||
)
|
||||
mark_utilized = columns.BooleanColumn(
|
||||
verbose_name=_('Marked Utilized')
|
||||
verbose_name=_('Marked Utilized'),
|
||||
false_mark=None
|
||||
)
|
||||
utilization = PrefixUtilizationColumn(
|
||||
verbose_name=_('Utilization'),
|
||||
@@ -314,7 +317,8 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
|
||||
linkify=True
|
||||
)
|
||||
mark_utilized = columns.BooleanColumn(
|
||||
verbose_name=_('Marked Utilized')
|
||||
verbose_name=_('Marked Utilized'),
|
||||
false_mark=None
|
||||
)
|
||||
utilization = columns.UtilizationColumn(
|
||||
verbose_name=_('Utilization'),
|
||||
@@ -386,7 +390,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
||||
assigned = columns.BooleanColumn(
|
||||
accessor='assigned_object_id',
|
||||
linkify=lambda record: record.assigned_object.get_absolute_url(),
|
||||
verbose_name=_('Assigned')
|
||||
verbose_name=_('Assigned'),
|
||||
false_mark=None
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
|
||||
@@ -211,6 +211,7 @@ class InterfaceVLANTable(NetBoxTable):
|
||||
)
|
||||
tagged = columns.BooleanColumn(
|
||||
verbose_name=_('Tagged'),
|
||||
false_mark=None
|
||||
)
|
||||
site = tables.Column(
|
||||
verbose_name=_('Site'),
|
||||
|
||||
@@ -30,7 +30,8 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name=_('RD')
|
||||
)
|
||||
enforce_unique = columns.BooleanColumn(
|
||||
verbose_name=_('Unique')
|
||||
verbose_name=_('Unique'),
|
||||
false_mark=None
|
||||
)
|
||||
import_targets = columns.TemplateColumn(
|
||||
verbose_name=_('Import Targets'),
|
||||
|
||||
@@ -49,12 +49,15 @@ AUTH_BACKEND_ATTRS = {
|
||||
'okta-openidconnect': ('Okta (OIDC)', None),
|
||||
'salesforce-oauth2': ('Salesforce', 'salesforce'),
|
||||
}
|
||||
# Override with potential user configuration
|
||||
AUTH_BACKEND_ATTRS.update(getattr(settings, 'SOCIAL_AUTH_BACKEND_ATTRS', {}))
|
||||
|
||||
|
||||
def get_auth_backend_display(name):
|
||||
"""
|
||||
Return the user-friendly name and icon name for a remote authentication backend, if known. Defaults to the
|
||||
raw backend name and no icon.
|
||||
Return the user-friendly name and icon name for a remote authentication backend, if
|
||||
known. Obtained from the defaults dictionary AUTH_BACKEND_ATTRS, overridden by the
|
||||
setting `SOCIAL_AUTH_BACKEND_ATTRS`. Defaults to the raw backend name and no icon.
|
||||
"""
|
||||
return AUTH_BACKEND_ATTRS.get(name, (name, None))
|
||||
|
||||
|
||||
@@ -81,10 +81,7 @@ class ColorChoices(ChoiceSet):
|
||||
#
|
||||
|
||||
class ButtonColorChoices(ChoiceSet):
|
||||
"""
|
||||
Map standard button color choices to Bootstrap 3 button classes
|
||||
"""
|
||||
DEFAULT = 'outline-dark'
|
||||
DEFAULT = 'default'
|
||||
BLUE = 'blue'
|
||||
INDIGO = 'indigo'
|
||||
PURPLE = 'purple'
|
||||
|
||||
@@ -60,6 +60,8 @@ class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms
|
||||
if value in self.fields[cf_name].empty_values:
|
||||
self.instance.custom_field_data[key] = None
|
||||
else:
|
||||
if customfield.type == CustomFieldTypeChoices.TYPE_JSON and type(value) is str:
|
||||
value = json.loads(value)
|
||||
self.instance.custom_field_data[key] = customfield.serialize(value)
|
||||
|
||||
return super().clean()
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import List
|
||||
import django_filters
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||
from strawberry import auto
|
||||
from ipam.fields import ASNField
|
||||
from netbox.graphql.scalars import BigInt
|
||||
@@ -201,4 +201,9 @@ def autotype_decorator(filterset):
|
||||
class BaseFilterMixin:
|
||||
|
||||
def filter_by_filterset(self, queryset, key):
|
||||
return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs
|
||||
filterset = self.filterset(data={key: getattr(self, key)}, queryset=queryset)
|
||||
if not filterset.is_valid():
|
||||
# We could raise validation error but strawberry logs it all to the
|
||||
# console i.e. raise ValidationError(f"{k}: {v[0]}")
|
||||
return filterset.qs.none()
|
||||
return filterset.qs
|
||||
|
||||
@@ -25,7 +25,7 @@ from utilities.string import trailing_slash
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '4.0.7'
|
||||
VERSION = '4.0.10'
|
||||
HOSTNAME = platform.node()
|
||||
# Set the base directory two levels up
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -739,11 +739,16 @@ RQ_QUEUES.update({
|
||||
|
||||
# Supported translation languages
|
||||
LANGUAGES = (
|
||||
('cs', _('Czech')),
|
||||
('da', _('Danish')),
|
||||
('de', _('German')),
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('fr', _('French')),
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
('nl', _('Dutch')),
|
||||
('pl', _('Polish')),
|
||||
('pt', _('Portuguese')),
|
||||
('ru', _('Russian')),
|
||||
('tr', _('Turkish')),
|
||||
@@ -758,6 +763,7 @@ LOCALE_PATHS = (
|
||||
# Strawberry (GraphQL)
|
||||
#
|
||||
STRAWBERRY_DJANGO = {
|
||||
"DEFAULT_PK_FIELD_NAME": "id",
|
||||
"TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True,
|
||||
"USE_DEPRECATED_FILTERS": True,
|
||||
}
|
||||
|
||||
@@ -194,14 +194,23 @@ class BooleanColumn(tables.Column):
|
||||
Custom implementation of BooleanColumn to render a nicely-formatted checkmark or X icon instead of a Unicode
|
||||
character.
|
||||
"""
|
||||
TRUE_MARK = mark_safe('<span class="text-success"><i class="mdi mdi-check-bold"></i></span>')
|
||||
FALSE_MARK = mark_safe('<span class="text-danger"><i class="mdi mdi-close-thick"></i></span>')
|
||||
EMPTY_MARK = mark_safe('<span class="text-muted">—</span>') # Placeholder
|
||||
|
||||
def __init__(self, *args, true_mark=TRUE_MARK, false_mark=FALSE_MARK, **kwargs):
|
||||
self.true_mark = true_mark
|
||||
self.false_mark = false_mark
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def render(self, value):
|
||||
if value:
|
||||
rendered = '<span class="text-success"><i class="mdi mdi-check-bold"></i></span>'
|
||||
elif value is None:
|
||||
rendered = '<span class="text-muted">—</span>'
|
||||
else:
|
||||
rendered = '<span class="text-danger"><i class="mdi mdi-close-thick"></i></span>'
|
||||
return mark_safe(rendered)
|
||||
if value is None:
|
||||
return self.EMPTY_MARK
|
||||
if value and self.true_mark:
|
||||
return self.true_mark
|
||||
if not value and self.false_mark:
|
||||
return self.false_mark
|
||||
return self.EMPTY_MARK
|
||||
|
||||
def value(self, value):
|
||||
return str(value)
|
||||
@@ -249,7 +258,7 @@ class ActionsColumn(tables.Column):
|
||||
|
||||
def render(self, record, table, **kwargs):
|
||||
# Skip dummy records (e.g. available VLANs) or those with no actions
|
||||
if not getattr(record, 'pk', None) or not self.actions:
|
||||
if not getattr(record, 'pk', None) or not (self.actions or self.extra_buttons):
|
||||
return ''
|
||||
|
||||
model = table.Meta.model
|
||||
@@ -321,19 +330,26 @@ class ActionsColumn(tables.Column):
|
||||
class ChoiceFieldColumn(tables.Column):
|
||||
"""
|
||||
Render a model's static ChoiceField with its value from `get_FOO_display()` as a colored badge. Background color is
|
||||
set by the instance's get_FOO_color() method, if defined.
|
||||
set by the instance's get_FOO_color() method, if defined, or can be overridden by a "color" callable.
|
||||
"""
|
||||
DEFAULT_BG_COLOR = 'secondary'
|
||||
|
||||
def __init__(self, *args, color=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.color = color
|
||||
|
||||
def render(self, record, bound_column, value):
|
||||
if value in self.empty_values:
|
||||
return self.default
|
||||
|
||||
# Determine the background color to use (try calling object.get_FOO_color())
|
||||
try:
|
||||
bg_color = getattr(record, f'get_{bound_column.name}_color')() or self.DEFAULT_BG_COLOR
|
||||
except AttributeError:
|
||||
bg_color = self.DEFAULT_BG_COLOR
|
||||
# Determine the background color to use (use "color" callable if given, else try calling object.get_FOO_color())
|
||||
if self.color:
|
||||
bg_color = self.color(record)
|
||||
else:
|
||||
try:
|
||||
bg_color = getattr(record, f'get_{bound_column.name}_color')() or self.DEFAULT_BG_COLOR
|
||||
except AttributeError:
|
||||
bg_color = self.DEFAULT_BG_COLOR
|
||||
|
||||
return mark_safe(f'<span class="badge text-bg-{bg_color}">{value}</span>')
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db.models.fields.related import RelatedField
|
||||
from django.db.models.fields.reverse_related import ManyToOneRel
|
||||
from django.urls import reverse
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -102,7 +103,7 @@ class BaseTable(tables.Table):
|
||||
field = model._meta.get_field(field_name)
|
||||
except FieldDoesNotExist:
|
||||
break
|
||||
if isinstance(field, RelatedField):
|
||||
if isinstance(field, (RelatedField, ManyToOneRel)):
|
||||
# Follow ForeignKeys to the related model
|
||||
prefetch_path.append(field_name)
|
||||
model = field.remote_field.model
|
||||
|
||||
@@ -13,11 +13,9 @@ class DummyModelType:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class DummyQuery:
|
||||
@strawberry.field
|
||||
def dummymodel(self, id: int) -> DummyModelType:
|
||||
return None
|
||||
dummymodel: DummyModelType = strawberry_django.field()
|
||||
dummymodel_list: List[DummyModelType] = strawberry_django.field()
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import json
|
||||
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from utilities.testing import disable_warnings, TestCase
|
||||
from core.models import ObjectType
|
||||
from dcim.models import Site, Location
|
||||
from users.models import ObjectPermission
|
||||
from utilities.testing import disable_warnings, APITestCase, TestCase
|
||||
|
||||
|
||||
class GraphQLTestCase(TestCase):
|
||||
@@ -34,3 +40,45 @@ class GraphQLTestCase(TestCase):
|
||||
response = self.client.get(url, **header)
|
||||
with disable_warnings('django.request'):
|
||||
self.assertHttpStatus(response, 302) # Redirect to login page
|
||||
|
||||
|
||||
class GraphQLAPITestCase(APITestCase):
|
||||
|
||||
@override_settings(LOGIN_REQUIRED=True)
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*', 'auth.user'])
|
||||
def test_graphql_filter_objects(self):
|
||||
"""
|
||||
Test the operation of filters for GraphQL API requests.
|
||||
"""
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
Location.objects.create(site=sites[0], name='Location 1', slug='location-1'),
|
||||
Location.objects.create(site=sites[1], name='Location 2', slug='location-2'),
|
||||
|
||||
# Add object-level permission
|
||||
obj_perm = ObjectPermission(
|
||||
name='Test permission',
|
||||
actions=['view']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(Location))
|
||||
|
||||
# A valid request should return the filtered list
|
||||
url = reverse('graphql')
|
||||
query = '{location_list(filters: {site_id: "' + str(sites[0].pk) + '"}) {id site {id}}}'
|
||||
response = self.client.post(url, data={'query': query}, format="json", **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotIn('errors', data)
|
||||
self.assertEqual(len(data['data']['location_list']), 1)
|
||||
|
||||
# An invalid request should return an empty list
|
||||
query = '{location_list(filters: {site_id: "99999"}) {id site {id}}}' # Invalid site ID
|
||||
response = self.client.post(url, data={'query': query}, format="json", **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
data = json.loads(response.content)
|
||||
self.assertEqual(len(data['data']['location_list']), 0)
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.test import override_settings
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.models import *
|
||||
from extras.models import CustomField
|
||||
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||
from users.models import ObjectPermission
|
||||
from utilities.testing import ModelViewTestCase, create_tags
|
||||
@@ -116,3 +117,28 @@ class CSVImportTestCase(ModelViewTestCase):
|
||||
# Test POST with permission
|
||||
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200)
|
||||
self.assertEqual(Region.objects.count(), 0)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_custom_field_defaults(self):
|
||||
self.add_permissions('dcim.add_region')
|
||||
csv_data = [
|
||||
'name,slug,description',
|
||||
'Region 1,region-1,abc',
|
||||
]
|
||||
data = {
|
||||
'format': ImportFormatChoices.CSV,
|
||||
'data': self._get_csv_data(csv_data),
|
||||
'csv_delimiter': CSVDelimiterChoices.AUTO,
|
||||
}
|
||||
|
||||
cf = CustomField.objects.create(
|
||||
name='tcf',
|
||||
type='text',
|
||||
required=False,
|
||||
default='def-cf-text'
|
||||
)
|
||||
cf.object_types.set([ObjectType.objects.get_for_model(self.model)])
|
||||
|
||||
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
|
||||
region = Region.objects.get(slug='region-1')
|
||||
self.assertEqual(region.cf['tcf'], 'def-cf-text')
|
||||
|
||||
@@ -4,20 +4,23 @@ from copy import deepcopy
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.fields import GenericRel
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db.models import ManyToManyField, ProtectedError, RestrictedError
|
||||
from django.db.models.fields.reverse_related import ManyToManyRel
|
||||
from django.forms import HiddenInput, ModelMultipleChoiceField, MultipleHiddenInput
|
||||
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
from django_tables2.export import TableExport
|
||||
from mptt.models import MPTTModel
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.models import ExportTemplate
|
||||
from extras.choices import CustomFieldUIEditableChoices
|
||||
from extras.models import CustomField, ExportTemplate
|
||||
from extras.signals import clear_events
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
||||
@@ -106,7 +109,13 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
try:
|
||||
return template.render_to_response(self.queryset)
|
||||
except Exception as e:
|
||||
messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}")
|
||||
messages.error(
|
||||
request,
|
||||
_("There was an error rendering the selected export template ({template}): {error}").format(
|
||||
template=template.name,
|
||||
error=e
|
||||
)
|
||||
)
|
||||
# Strip the `export` param and redirect user to the filtered objects list
|
||||
query_params = request.GET.copy()
|
||||
query_params.pop('export')
|
||||
@@ -170,6 +179,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
table.columns.hide('pk')
|
||||
return render(request, 'htmx/table.html', {
|
||||
'table': table,
|
||||
'model': model,
|
||||
'actions': actions,
|
||||
})
|
||||
|
||||
context = {
|
||||
@@ -409,6 +420,17 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
if instance.pk and hasattr(instance, 'snapshot'):
|
||||
instance.snapshot()
|
||||
|
||||
else:
|
||||
# For newly created objects, apply any default custom field values
|
||||
custom_fields = CustomField.objects.filter(
|
||||
object_types=ContentType.objects.get_for_model(self.queryset.model),
|
||||
ui_editable=CustomFieldUIEditableChoices.YES
|
||||
)
|
||||
for cf in custom_fields:
|
||||
field_name = f'cf_{cf.name}'
|
||||
if field_name not in record:
|
||||
record[field_name] = cf.default
|
||||
|
||||
# Instantiate the model form for the object
|
||||
model_form_kwargs = {
|
||||
'data': record,
|
||||
@@ -593,6 +615,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
if form.cleaned_data.get('remove_tags', None):
|
||||
obj.tags.remove(*form.cleaned_data['remove_tags'])
|
||||
|
||||
# Rebuild the tree for MPTT models
|
||||
if issubclass(self.queryset.model, MPTTModel):
|
||||
self.queryset.model.objects.rebuild()
|
||||
|
||||
return updated_objects
|
||||
|
||||
#
|
||||
@@ -668,7 +694,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
# Retrieve objects being edited
|
||||
table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
|
||||
if not table.rows:
|
||||
messages.warning(request, "No {} were selected.".format(model._meta.verbose_name_plural))
|
||||
messages.warning(
|
||||
request,
|
||||
_("No {object_type} were selected.").format(object_type=model._meta.verbose_name_plural)
|
||||
)
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
@@ -745,8 +774,13 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):
|
||||
raise PermissionsViolation
|
||||
|
||||
model_name = self.queryset.model._meta.verbose_name_plural
|
||||
messages.success(request, f"Renamed {len(selected_objects)} {model_name}")
|
||||
messages.success(
|
||||
request,
|
||||
_("Renamed {count} {object_type}").format(
|
||||
count=len(selected_objects),
|
||||
object_type=self.queryset.model._meta.verbose_name_plural
|
||||
)
|
||||
)
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
except (AbortRequest, PermissionsViolation) as e:
|
||||
@@ -838,7 +872,10 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
messages.error(request, mark_safe(e.message))
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
|
||||
msg = _("Deleted {count} {object_type}").format(
|
||||
count=deleted_count,
|
||||
object_type=model._meta.verbose_name_plural
|
||||
)
|
||||
logger.info(msg)
|
||||
messages.success(request, msg)
|
||||
return redirect(self.get_return_url(request))
|
||||
@@ -855,7 +892,10 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
# Retrieve objects being deleted
|
||||
table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
|
||||
if not table.rows:
|
||||
messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
|
||||
messages.warning(
|
||||
request,
|
||||
_("No {object_type} were selected.").format(object_type=model._meta.verbose_name_plural)
|
||||
)
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
@@ -900,7 +940,10 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
|
||||
selected_objects = self.parent_model.objects.filter(pk__in=pk_list)
|
||||
if not selected_objects:
|
||||
messages.warning(request, "No {} were selected.".format(self.parent_model._meta.verbose_name_plural))
|
||||
messages.warning(
|
||||
request,
|
||||
_("No {object_type} were selected.").format(object_type=self.parent_model._meta.verbose_name_plural)
|
||||
)
|
||||
return redirect(self.get_return_url(request))
|
||||
table = self.table(selected_objects, orderable=False)
|
||||
|
||||
|
||||
@@ -202,11 +202,14 @@ class ObjectSyncDataView(View):
|
||||
obj = get_object_or_404(qs, **kwargs)
|
||||
|
||||
if not obj.data_file:
|
||||
messages.error(request, f"Unable to synchronize data: No data file set.")
|
||||
messages.error(request, _("Unable to synchronize data: No data file set."))
|
||||
return redirect(obj.get_absolute_url())
|
||||
|
||||
obj.sync(save=True)
|
||||
messages.success(request, f"Synchronized data for {model._meta.verbose_name} {obj}.")
|
||||
messages.success(request, _("Synchronized data for {object_type} {object}.").format(
|
||||
object_type=model._meta.verbose_name,
|
||||
object=obj
|
||||
))
|
||||
|
||||
return redirect(obj.get_absolute_url())
|
||||
|
||||
@@ -228,7 +231,9 @@ class BulkSyncDataView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
for obj in selected_objects:
|
||||
obj.sync(save=True)
|
||||
|
||||
model_name = self.queryset.model._meta.verbose_name_plural
|
||||
messages.success(request, f"Synced {len(selected_objects)} {model_name}")
|
||||
messages.success(request, _("Synced {count} {object_type}").format(
|
||||
count=len(selected_objects),
|
||||
object_type=self.queryset.model._meta.verbose_name_plural
|
||||
))
|
||||
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
67
netbox/project-static/dist/netbox.js
vendored
67
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -27,29 +27,29 @@
|
||||
"bootstrap": "5.3.3",
|
||||
"clipboard": "2.0.11",
|
||||
"flatpickr": "4.6.13",
|
||||
"gridstack": "10.3.0",
|
||||
"gridstack": "10.3.1",
|
||||
"htmx.org": "1.9.12",
|
||||
"query-string": "9.0.0",
|
||||
"sass": "1.77.6",
|
||||
"query-string": "9.1.0",
|
||||
"sass": "1.77.8",
|
||||
"tom-select": "2.3.1",
|
||||
"typeface-inter": "3.18.1",
|
||||
"typeface-roboto-mono": "1.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/node": "^20.11.16",
|
||||
"@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",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^22.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.1.0",
|
||||
"@typescript-eslint/parser": "^8.1.0",
|
||||
"esbuild": "^0.23.0",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"eslint": "<9.0",
|
||||
"eslint-config-prettier": "^9.1.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"
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "<5.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/bootstrap/**/@popperjs/core": "^2.11.6"
|
||||
|
||||
@@ -39,10 +39,17 @@ export function initFormElements(): void {
|
||||
// Find each of the form's submitters. Most object edit forms have a "Create" and
|
||||
// a "Create & Add", so we need to add a listener to both.
|
||||
const submitters = form.querySelectorAll<HTMLButtonElement>('button[type=submit]');
|
||||
|
||||
for (const submitter of submitters) {
|
||||
// Add the event listener to each submitter.
|
||||
submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form));
|
||||
}
|
||||
|
||||
// Initialize any reset buttons so that when clicked, the page is reloaded without query parameters.
|
||||
const resetButton = document.querySelector<HTMLButtonElement>('button[data-reset-select]');
|
||||
if (resetButton !== null) {
|
||||
resetButton.addEventListener('click', () => {
|
||||
window.location.assign(window.location.origin + window.location.pathname);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { initFormElements } from './elements';
|
||||
import { initSpeedSelector } from './speedSelector';
|
||||
import { initScopeSelector } from './scopeSelector';
|
||||
|
||||
export function initForms(): void {
|
||||
for (const func of [initFormElements, initSpeedSelector, initScopeSelector]) {
|
||||
for (const func of [initFormElements, initSpeedSelector]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
import { getElements, toggleVisibility } from '../util';
|
||||
|
||||
type ShowHideMap = {
|
||||
/**
|
||||
* Name of view to which this map should apply.
|
||||
*
|
||||
* @example vlangroup_edit
|
||||
*/
|
||||
[view: string]: string;
|
||||
};
|
||||
|
||||
type ShowHideLayout = {
|
||||
/**
|
||||
* Name of layout config
|
||||
*
|
||||
* @example vlangroup
|
||||
*/
|
||||
[config: string]: {
|
||||
/**
|
||||
* Default layout.
|
||||
*/
|
||||
default: { hide: string[]; show: string[] };
|
||||
/**
|
||||
* Field name to layout mapping.
|
||||
*/
|
||||
[fieldName: string]: { hide: string[]; show: string[] };
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping of layout names to arrays of object types whose fields should be hidden or shown when
|
||||
* the scope type (key) is selected.
|
||||
*
|
||||
* For example, if `region` is the scope type, the fields with IDs listed in
|
||||
* showHideMap.region.hide should be hidden, and the fields with IDs listed in
|
||||
* showHideMap.region.show should be shown.
|
||||
*/
|
||||
const showHideLayout: ShowHideLayout = {
|
||||
vlangroup: {
|
||||
region: {
|
||||
hide: ['id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region'],
|
||||
},
|
||||
'site group': {
|
||||
hide: ['id_region', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_sitegroup'],
|
||||
},
|
||||
site: {
|
||||
hide: ['id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region', 'id_sitegroup', 'id_site'],
|
||||
},
|
||||
location: {
|
||||
hide: ['id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location'],
|
||||
},
|
||||
rack: {
|
||||
hide: ['id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
||||
},
|
||||
'cluster group': {
|
||||
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_cluster'],
|
||||
show: ['id_clustergroup'],
|
||||
},
|
||||
cluster: {
|
||||
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
||||
show: ['id_clustergroup', 'id_cluster'],
|
||||
},
|
||||
default: {
|
||||
hide: [
|
||||
'id_region',
|
||||
'id_sitegroup',
|
||||
'id_site',
|
||||
'id_location',
|
||||
'id_rack',
|
||||
'id_clustergroup',
|
||||
'id_cluster',
|
||||
],
|
||||
show: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping of view names to layout configurations
|
||||
*
|
||||
* For example, if `vlangroup_add` is the view, use the layout configuration `vlangroup`.
|
||||
*/
|
||||
const showHideMap: ShowHideMap = {
|
||||
vlangroup_add: 'vlangroup',
|
||||
vlangroup_edit: 'vlangroup',
|
||||
vlangroup_bulk_edit: 'vlangroup',
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle visibility of a given element's parent.
|
||||
* @param query CSS Query.
|
||||
* @param action Show or Hide the Parent.
|
||||
*/
|
||||
function toggleParentVisibility(query: string, action: 'show' | 'hide') {
|
||||
for (const element of getElements(query)) {
|
||||
const parent = element.parentElement?.parentElement as Nullable<HTMLDivElement>;
|
||||
if (parent !== null) {
|
||||
if (action === 'show') {
|
||||
toggleVisibility(parent, 'show');
|
||||
} else {
|
||||
toggleVisibility(parent, 'hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle changes to the Scope Type field.
|
||||
*/
|
||||
function handleScopeChange<P extends keyof ShowHideMap>(view: P, element: HTMLSelectElement) {
|
||||
// Scope type's innerText looks something like `DCIM > region`.
|
||||
const scopeType = element.options[element.selectedIndex].innerText.toLowerCase();
|
||||
const layoutConfig = showHideMap[view];
|
||||
|
||||
for (const [scope, fields] of Object.entries(showHideLayout[layoutConfig])) {
|
||||
// If the scope type ends with the specified scope, toggle its field visibility according to
|
||||
// the show/hide values.
|
||||
if (scopeType.endsWith(scope)) {
|
||||
for (const field of fields.hide) {
|
||||
toggleParentVisibility(`#${field}`, 'hide');
|
||||
}
|
||||
for (const field of fields.show) {
|
||||
toggleParentVisibility(`#${field}`, 'show');
|
||||
}
|
||||
// Stop on first match.
|
||||
break;
|
||||
} else {
|
||||
// Otherwise, hide all fields.
|
||||
for (const field of showHideLayout[layoutConfig].default.hide) {
|
||||
toggleParentVisibility(`#${field}`, 'hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize scope type select event listeners.
|
||||
*/
|
||||
export function initScopeSelector(): void {
|
||||
for (const view of Object.keys(showHideMap)) {
|
||||
for (const element of getElements<HTMLSelectElement>(
|
||||
`html[data-netbox-url-name="${view}"] #id_scope_type`,
|
||||
)) {
|
||||
handleScopeChange(view, element);
|
||||
element.addEventListener('change', () => handleScopeChange(view, element));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ export function initMessages(): void {
|
||||
for (const element of elements) {
|
||||
if (element !== null) {
|
||||
const toast = new Toast(element);
|
||||
toast.show();
|
||||
if (!toast.isShown()) {
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
|
||||
// Remove the bottom margin of <p> elements inside a table cell
|
||||
td > .rendered-markdown {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -20,3 +20,14 @@ hr.dropdown-divider {
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
// Restore support for old Bootstrap v3 colors
|
||||
.text-bg-black {
|
||||
@extend .text-bg-dark;
|
||||
}
|
||||
.text-bg-gray {
|
||||
@extend .text-bg-secondary;
|
||||
}
|
||||
.text-bg-white {
|
||||
@extend .text-bg-light;
|
||||
}
|
||||
|
||||
@@ -44,3 +44,20 @@ table a {
|
||||
[data-bs-theme=dark] ::selection {
|
||||
background-color: rgba(var(--tblr-primary-rgb),.48)
|
||||
}
|
||||
|
||||
// Do not apply padding to <code> elements inside a <pre>
|
||||
pre code {
|
||||
padding: unset;
|
||||
}
|
||||
|
||||
// Use an icon instead of Tabler's native "caret" for dropdowns (avoids a Safari bug)
|
||||
.dropdown-toggle:after{
|
||||
font-family: "Material Design Icons";
|
||||
content: '\F0140';
|
||||
padding-right: 9px;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
transform: none;
|
||||
vertical-align: .05em;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -21,17 +21,137 @@
|
||||
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
|
||||
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0":
|
||||
"@esbuild/aix-ppc64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259"
|
||||
integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==
|
||||
|
||||
"@esbuild/android-arm64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832"
|
||||
integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==
|
||||
|
||||
"@esbuild/android-arm@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99"
|
||||
integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==
|
||||
|
||||
"@esbuild/android-x64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6"
|
||||
integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==
|
||||
|
||||
"@esbuild/darwin-arm64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e"
|
||||
integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==
|
||||
|
||||
"@esbuild/darwin-x64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c"
|
||||
integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4"
|
||||
integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==
|
||||
|
||||
"@esbuild/freebsd-x64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d"
|
||||
integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==
|
||||
|
||||
"@esbuild/linux-arm64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a"
|
||||
integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==
|
||||
|
||||
"@esbuild/linux-arm@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad"
|
||||
integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==
|
||||
|
||||
"@esbuild/linux-ia32@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238"
|
||||
integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==
|
||||
|
||||
"@esbuild/linux-loong64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280"
|
||||
integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==
|
||||
|
||||
"@esbuild/linux-mips64el@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5"
|
||||
integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==
|
||||
|
||||
"@esbuild/linux-ppc64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6"
|
||||
integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==
|
||||
|
||||
"@esbuild/linux-riscv64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780"
|
||||
integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==
|
||||
|
||||
"@esbuild/linux-s390x@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8"
|
||||
integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==
|
||||
|
||||
"@esbuild/linux-x64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910"
|
||||
integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==
|
||||
|
||||
"@esbuild/netbsd-x64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c"
|
||||
integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db"
|
||||
integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8"
|
||||
integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==
|
||||
|
||||
"@esbuild/sunos-x64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8"
|
||||
integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==
|
||||
|
||||
"@esbuild/win32-arm64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d"
|
||||
integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7"
|
||||
integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==
|
||||
|
||||
"@esbuild/win32-x64@0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced"
|
||||
integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1":
|
||||
version "4.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
|
||||
integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
|
||||
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1":
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae"
|
||||
integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==
|
||||
|
||||
"@eslint/eslintrc@^2.1.4":
|
||||
version "2.1.4"
|
||||
@@ -239,6 +359,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz#6d2f812e3b19545bba2d81caffff1204de9a6a58"
|
||||
integrity sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ==
|
||||
|
||||
"@pkgr/core@^0.1.0":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
|
||||
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
|
||||
|
||||
"@popperjs/core@^2.11.6", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2":
|
||||
version "2.11.8"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
|
||||
@@ -581,37 +706,27 @@
|
||||
dependencies:
|
||||
"@types/tern" "*"
|
||||
|
||||
"@types/cookie@^0.5.1":
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.5.4.tgz#7e70a20cd695bc48d46b08c2505874cd68b760e0"
|
||||
integrity sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==
|
||||
"@types/cookie@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
|
||||
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
|
||||
|
||||
"@types/estree@*":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||
|
||||
"@types/json-schema@^7.0.9":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
"@types/json5@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/node@^20.11.16":
|
||||
version "20.12.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.8.tgz#35897bf2bfe3469847ab04634636de09552e8256"
|
||||
integrity sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==
|
||||
"@types/node@^22.3.0":
|
||||
version "22.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.3.0.tgz#7f8da0e2b72c27c4f9bd3cb5ef805209d04d4f9e"
|
||||
integrity sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/semver@^7.3.12":
|
||||
version "7.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
|
||||
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
|
||||
undici-types "~6.18.2"
|
||||
|
||||
"@types/tern@*":
|
||||
version "0.23.9"
|
||||
@@ -620,89 +735,86 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.39.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db"
|
||||
integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==
|
||||
"@typescript-eslint/eslint-plugin@^8.1.0":
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.1.0.tgz#3c020deeaaba82a6f741d00dacf172c53be4911f"
|
||||
integrity sha512-LlNBaHFCEBPHyD4pZXb35mzjGkuGKXU5eeCA1SxvHfiRES0E82dOounfVpL4DCqYvJEKab0bZIA0gCRpdLKkCw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "5.62.0"
|
||||
"@typescript-eslint/type-utils" "5.62.0"
|
||||
"@typescript-eslint/utils" "5.62.0"
|
||||
debug "^4.3.4"
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.1.0"
|
||||
"@typescript-eslint/type-utils" "8.1.0"
|
||||
"@typescript-eslint/utils" "8.1.0"
|
||||
"@typescript-eslint/visitor-keys" "8.1.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.2.0"
|
||||
natural-compare-lite "^1.4.0"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
ignore "^5.3.1"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/parser@^5.39.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7"
|
||||
integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==
|
||||
"@typescript-eslint/parser@^8.1.0":
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.1.0.tgz#b7e77f5fa212df59eba51ecd4986f194bccc2303"
|
||||
integrity sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.62.0"
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/typescript-estree" "5.62.0"
|
||||
"@typescript-eslint/scope-manager" "8.1.0"
|
||||
"@typescript-eslint/types" "8.1.0"
|
||||
"@typescript-eslint/typescript-estree" "8.1.0"
|
||||
"@typescript-eslint/visitor-keys" "8.1.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c"
|
||||
integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==
|
||||
"@typescript-eslint/scope-manager@8.1.0":
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.1.0.tgz#dd8987d2efebb71d230a1c71d82e84a7aead5c3d"
|
||||
integrity sha512-DsuOZQji687sQUjm4N6c9xABJa7fjvfIdjqpSIIVOgaENf2jFXiM9hIBZOL3hb6DHK9Nvd2d7zZnoMLf9e0OtQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/visitor-keys" "5.62.0"
|
||||
"@typescript-eslint/types" "8.1.0"
|
||||
"@typescript-eslint/visitor-keys" "8.1.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a"
|
||||
integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==
|
||||
"@typescript-eslint/type-utils@8.1.0":
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.1.0.tgz#dbf5a4308166dfc37a36305390dea04a3a3b5048"
|
||||
integrity sha512-oLYvTxljVvsMnldfl6jIKxTaU7ok7km0KDrwOt1RHYu6nxlhN3TIx8k5Q52L6wR33nOwDgM7VwW1fT1qMNfFIA==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.62.0"
|
||||
"@typescript-eslint/utils" "5.62.0"
|
||||
"@typescript-eslint/typescript-estree" "8.1.0"
|
||||
"@typescript-eslint/utils" "8.1.0"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/types@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
|
||||
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
|
||||
"@typescript-eslint/types@8.1.0":
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.1.0.tgz#fbf1eaa668a7e444ac507732ca9d3c3468e5db9c"
|
||||
integrity sha512-q2/Bxa0gMOu/2/AKALI0tCKbG2zppccnRIRCW6BaaTlRVaPKft4oVYPp7WOPpcnsgbr0qROAVCVKCvIQ0tbWog==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
|
||||
integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==
|
||||
"@typescript-eslint/typescript-estree@8.1.0":
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.1.0.tgz#c44e5667683c0bb5caa43192e27de6a994f4e4c4"
|
||||
integrity sha512-NTHhmufocEkMiAord/g++gWKb0Fr34e9AExBRdqgWdVBaKoei2dIyYKD9Q0jBnvfbEA5zaf8plUFMUH6kQ0vGg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/visitor-keys" "5.62.0"
|
||||
"@typescript-eslint/types" "8.1.0"
|
||||
"@typescript-eslint/visitor-keys" "8.1.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/utils@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
|
||||
integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
|
||||
"@typescript-eslint/utils@8.1.0":
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.1.0.tgz#a922985a43d2560ce0d293be79148fa80c1325e0"
|
||||
integrity sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "5.62.0"
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/typescript-estree" "5.62.0"
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.1.0"
|
||||
"@typescript-eslint/types" "8.1.0"
|
||||
"@typescript-eslint/typescript-estree" "8.1.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
|
||||
integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==
|
||||
"@typescript-eslint/visitor-keys@8.1.0":
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.1.0.tgz#ab2b3a9699a8ddebf0c205e133f114c1fed9daad"
|
||||
integrity sha512-ba0lNI19awqZ5ZNKh6wCModMwoZs457StTebQ0q1NP58zSi2F6MOZRXwfKZy+jB78JNJ/WH8GSh2IQNzXX8Nag==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
"@typescript-eslint/types" "8.1.0"
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
version "1.2.0"
|
||||
@@ -715,9 +827,9 @@ acorn-jsx@^5.3.2:
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn@^8.9.0:
|
||||
version "8.11.3"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
|
||||
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
|
||||
version "8.12.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
|
||||
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
|
||||
|
||||
ajv@^6.12.4:
|
||||
version "6.12.6"
|
||||
@@ -867,13 +979,27 @@ brace-expansion@^1.1.7:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@^3.0.2, braces@~3.0.2:
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
|
||||
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
braces@~3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
||||
@@ -1012,7 +1138,14 @@ debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
|
||||
debug@^4.3.1, debug@^4.3.2:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b"
|
||||
integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
@@ -1187,131 +1320,54 @@ es-to-primitive@^1.2.1:
|
||||
is-date-object "^1.0.1"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
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-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-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-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-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-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-64@0.13.15:
|
||||
version "0.13.15"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3"
|
||||
integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==
|
||||
|
||||
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-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-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-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-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-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-sass-plugin@^2.3.3:
|
||||
version "2.16.1"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.1.tgz#4f46cef84675ec3c5e7a93256d6c67527cfdb4d0"
|
||||
integrity sha512-mBB2aEF0xk7yo+Q9pSUh8xYED/1O2wbAM6IauGkDrqy6pl9SbJNakLeLGXiNpNujWIudu8TJTZCv2L5AQYRXtA==
|
||||
esbuild-sass-plugin@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-3.3.1.tgz#74d096127bff10072042de30119235f276655566"
|
||||
integrity sha512-SnO1ls+d52n6j8gRRpjexXI8MsHEaumS0IdDHaYM29Y6gakzZYMls6i9ql9+AWMSQk/eryndmUpXEgT34QrX1A==
|
||||
dependencies:
|
||||
resolve "^1.22.6"
|
||||
sass "^1.7.3"
|
||||
resolve "^1.22.8"
|
||||
safe-identifier "^0.4.2"
|
||||
sass "^1.71.1"
|
||||
|
||||
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-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-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-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@^0.13.15:
|
||||
version "0.13.15"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf"
|
||||
integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==
|
||||
esbuild@^0.23.0:
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599"
|
||||
integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==
|
||||
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"
|
||||
"@esbuild/aix-ppc64" "0.23.0"
|
||||
"@esbuild/android-arm" "0.23.0"
|
||||
"@esbuild/android-arm64" "0.23.0"
|
||||
"@esbuild/android-x64" "0.23.0"
|
||||
"@esbuild/darwin-arm64" "0.23.0"
|
||||
"@esbuild/darwin-x64" "0.23.0"
|
||||
"@esbuild/freebsd-arm64" "0.23.0"
|
||||
"@esbuild/freebsd-x64" "0.23.0"
|
||||
"@esbuild/linux-arm" "0.23.0"
|
||||
"@esbuild/linux-arm64" "0.23.0"
|
||||
"@esbuild/linux-ia32" "0.23.0"
|
||||
"@esbuild/linux-loong64" "0.23.0"
|
||||
"@esbuild/linux-mips64el" "0.23.0"
|
||||
"@esbuild/linux-ppc64" "0.23.0"
|
||||
"@esbuild/linux-riscv64" "0.23.0"
|
||||
"@esbuild/linux-s390x" "0.23.0"
|
||||
"@esbuild/linux-x64" "0.23.0"
|
||||
"@esbuild/netbsd-x64" "0.23.0"
|
||||
"@esbuild/openbsd-arm64" "0.23.0"
|
||||
"@esbuild/openbsd-x64" "0.23.0"
|
||||
"@esbuild/sunos-x64" "0.23.0"
|
||||
"@esbuild/win32-arm64" "0.23.0"
|
||||
"@esbuild/win32-ia32" "0.23.0"
|
||||
"@esbuild/win32-x64" "0.23.0"
|
||||
|
||||
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"
|
||||
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
||||
|
||||
eslint-config-prettier@^8.5.0:
|
||||
version "8.10.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11"
|
||||
integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==
|
||||
eslint-config-prettier@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f"
|
||||
integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==
|
||||
|
||||
eslint-import-resolver-node@^0.3.9:
|
||||
version "0.3.9"
|
||||
@@ -1365,20 +1421,13 @@ eslint-plugin-import@^2.26.0:
|
||||
semver "^6.3.1"
|
||||
tsconfig-paths "^3.15.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"
|
||||
integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
|
||||
eslint-plugin-prettier@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz#d1c8f972d8f60e414c25465c163d16f209411f95"
|
||||
integrity sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==
|
||||
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"
|
||||
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^4.1.1"
|
||||
synckit "^0.9.1"
|
||||
|
||||
eslint-scope@^7.2.2:
|
||||
version "7.2.2"
|
||||
@@ -1393,7 +1442,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
|
||||
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||
|
||||
eslint@^8.24.0:
|
||||
eslint@<9.0:
|
||||
version "8.57.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668"
|
||||
integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==
|
||||
@@ -1447,9 +1496,9 @@ espree@^9.6.0, espree@^9.6.1:
|
||||
eslint-visitor-keys "^3.4.1"
|
||||
|
||||
esquery@^1.4.2:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
|
||||
integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
|
||||
integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
|
||||
dependencies:
|
||||
estraverse "^5.1.0"
|
||||
|
||||
@@ -1460,11 +1509,6 @@ esrecurse@^4.3.0:
|
||||
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"
|
||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||
|
||||
estraverse@^5.1.0, estraverse@^5.2.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
|
||||
@@ -1520,10 +1564,10 @@ file-entry-cache@^6.0.1:
|
||||
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"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
fill-range@^7.0.1, fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
@@ -1754,10 +1798,10 @@ graphql@16.8.1:
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gridstack@10.3.0:
|
||||
version "10.3.0"
|
||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.3.0.tgz#8fa065f896d0a880c5c54c24d189f3197184488a"
|
||||
integrity sha512-eGKsmU2TppV4coyDu9IIdIkm4qjgLLdjlEOFwQyQMuSwfOpzSfLdPc8du0HuebGr7CvAIrJxN4lBOmGrWSBg9g==
|
||||
gridstack@10.3.1:
|
||||
version "10.3.1"
|
||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.3.1.tgz#4ed704279c40094fc1b9e3318f20b573f2fe9f40"
|
||||
integrity sha512-Ra82k/88gdeiu3ZP40COS4bI4sGhNQlZAaAQ6szfPfr68zVpsXxiyLKr5zYcTpKX4jjcwyNsNNdcV1tDJc71fA==
|
||||
|
||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -1810,15 +1854,15 @@ htmx.org@1.9.12:
|
||||
resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-1.9.12.tgz#1c5bc7fb4d3eb4e8c0d72323dc774a6b9b66addc"
|
||||
integrity sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw==
|
||||
|
||||
ignore@^5.2.0:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
|
||||
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
|
||||
ignore@^5.2.0, ignore@^5.3.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
|
||||
|
||||
immutable@^4.0.0:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447"
|
||||
integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
|
||||
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
|
||||
|
||||
import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
@@ -2102,13 +2146,6 @@ loose-envify@^1.0.0, loose-envify@^1.1.0:
|
||||
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"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
markdown-it@^14.1.0:
|
||||
version "14.1.0"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
|
||||
@@ -2151,6 +2188,13 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^9.0.4:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
|
||||
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
@@ -2166,11 +2210,6 @@ ms@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
natural-compare-lite@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
|
||||
integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
@@ -2331,10 +2370,10 @@ prettier-linter-helpers@^1.0.0:
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@^2.7.1:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
prettier@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
|
||||
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
|
||||
|
||||
punycode.js@^2.3.1:
|
||||
version "2.3.1"
|
||||
@@ -2346,10 +2385,10 @@ punycode@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
query-string@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.0.0.tgz#1fe177cd95545600f0deab93f5fb02fd4e3e7273"
|
||||
integrity sha512-4EWwcRGsO2H+yzq6ddHcVqkCQ2EFUSfDMEjF8ryp8ReymyZhIuaFRGLomeOQLkrzacMHoyky2HW0Qe30UbzkKw==
|
||||
query-string@9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.1.0.tgz#5f12a4653a4ba56021e113b5cf58e56581823e7a"
|
||||
integrity sha512-t6dqMECpCkqfyv2FfwVS1xcB6lgXW/0XZSaKdsCNGYkqMO76AFiJEg4vINzoDKcZa6MS7JX+OHIjwh06K5vczw==
|
||||
dependencies:
|
||||
decode-uri-component "^0.4.1"
|
||||
filter-obj "^5.1.0"
|
||||
@@ -2435,7 +2474,7 @@ resolve-pkg-maps@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f"
|
||||
integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==
|
||||
|
||||
resolve@^1.22.4, resolve@^1.22.6:
|
||||
resolve@^1.22.4, resolve@^1.22.8:
|
||||
version "1.22.8"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
|
||||
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
|
||||
@@ -2473,6 +2512,11 @@ safe-array-concat@^1.1.2:
|
||||
has-symbols "^1.0.3"
|
||||
isarray "^2.0.5"
|
||||
|
||||
safe-identifier@^0.4.2:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb"
|
||||
integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==
|
||||
|
||||
safe-regex-test@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377"
|
||||
@@ -2482,19 +2526,10 @@ safe-regex-test@^1.0.3:
|
||||
es-errors "^1.3.0"
|
||||
is-regex "^1.1.4"
|
||||
|
||||
sass@1.77.6:
|
||||
version "1.77.6"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.6.tgz#898845c1348078c2e6d1b64f9ee06b3f8bd489e4"
|
||||
integrity sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
immutable "^4.0.0"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
|
||||
sass@^1.7.3:
|
||||
version "1.76.0"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.76.0.tgz#fe15909500735ac154f0dc7386d656b62b03987d"
|
||||
integrity sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==
|
||||
sass@1.77.8, sass@^1.71.1:
|
||||
version "1.77.8"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.8.tgz#9f18b449ea401759ef7ec1752a16373e296b52bd"
|
||||
integrity sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
immutable "^4.0.0"
|
||||
@@ -2517,12 +2552,10 @@ semver@^6.3.1:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
semver@^7.3.7:
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
|
||||
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
semver@^7.6.0:
|
||||
version "7.6.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
|
||||
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
|
||||
|
||||
set-function-length@^1.2.1:
|
||||
version "1.2.2"
|
||||
@@ -2656,6 +2689,14 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
synckit@^0.9.1:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.1.tgz#febbfbb6649979450131f64735aa3f6c14575c88"
|
||||
integrity sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.1.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
tapable@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
|
||||
@@ -2691,6 +2732,11 @@ tom-select@2.3.1:
|
||||
"@orchidjs/sifter" "^1.0.3"
|
||||
"@orchidjs/unicode-variants" "^1.0.4"
|
||||
|
||||
ts-api-utils@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
|
||||
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
|
||||
|
||||
tsconfig-paths@^3.15.0:
|
||||
version "3.15.0"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
|
||||
@@ -2701,22 +2747,15 @@ tsconfig-paths@^3.15.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^1.8.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.1:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
tslib@^2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
|
||||
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
@@ -2784,10 +2823,10 @@ typeface-roboto-mono@1.1.13:
|
||||
resolved "https://registry.yarnpkg.com/typeface-roboto-mono/-/typeface-roboto-mono-1.1.13.tgz#2af8662db8f9119c00efd55d6ed8877d2a69ec94"
|
||||
integrity sha512-pnzDc70b7ywJHin/BUFL7HZX8DyOTBLT2qxlJ92eH1UJOFcENIBXa9IZrxsJX/gEKjbEDKhW5vz/TKRBNk/ufQ==
|
||||
|
||||
typescript@~4.8.4:
|
||||
version "4.8.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
|
||||
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
|
||||
typescript@<5.5:
|
||||
version "5.4.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
|
||||
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
|
||||
|
||||
uc.micro@^2.0.0, uc.micro@^2.1.0:
|
||||
version "2.1.0"
|
||||
@@ -2804,10 +2843,10 @@ unbox-primitive@^1.0.2:
|
||||
has-symbols "^1.0.3"
|
||||
which-boxed-primitive "^1.0.2"
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
undici-types@~6.18.2:
|
||||
version "6.18.2"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.18.2.tgz#8b678cf939d4fc9ec56be3c68ed69c619dee28b0"
|
||||
integrity sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.4.1"
|
||||
@@ -2875,11 +2914,6 @@ wrappy@1:
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
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"
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
<html
|
||||
lang="en"
|
||||
data-netbox-url-name="{{ request.resolver_match.url_name }}"
|
||||
data-netbox-base-path="{{ settings.BASE_PATH }}"
|
||||
data-netbox-version="{{ settings.VERSION }}"
|
||||
{% if request.user.is_authenticated %}
|
||||
data-netbox-user-name="{{ request.user.username }}"
|
||||
data-netbox-user-id="{{ request.user.pk }}"
|
||||
{% endif %}
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<tr>
|
||||
<th scope="row" class="ps-3">{% trans "Custom validators" %}</th>
|
||||
{% if config.CUSTOM_VALIDATORS %}
|
||||
<td><pre>{{ config.CUSTOM_VALIDATORS|json }}</pre></td>
|
||||
<td><pre>{{ config.CUSTOM_VALIDATORS }}</pre></td>
|
||||
{% else %}
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
{% load perms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object|meta:"verbose_name_plural"|bettertitle }}</a>
|
||||
</li>
|
||||
{% with parent_jobs_viewname=object.object|viewname:"jobs" %}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url parent_jobs_viewname pk=object.object.pk %}">{{ object.object }}</a>
|
||||
</li>
|
||||
{% endwith %}
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
{% block control-buttons %}
|
||||
{% if request.user|can_delete:object %}
|
||||
{% delete_button object %}
|
||||
|
||||
@@ -24,7 +24,12 @@
|
||||
</div>
|
||||
{% endblock page-header %}
|
||||
|
||||
{% block title %}{{ status|capfirst }} {% trans "Workers in " %}{{ queue.name }}{% endblock %}
|
||||
{% block title %}
|
||||
{{ status|capfirst }}
|
||||
{% blocktrans trimmed with queue_name=queue.name %}
|
||||
Workers in {{ queue_name }}
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block controls %}{% endblock %}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<td class="d-flex justify-content-between align-items-start">
|
||||
{% if object.rack %}
|
||||
{{ object.rack|linkify }}
|
||||
<a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}">
|
||||
<a href="{{ object.rack.get_absolute_url }}?device={% firstof object.parent_bay.device.pk object.pk %}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}">
|
||||
<i class="mdi mdi-view-day-outline"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
|
||||
@@ -9,31 +9,35 @@
|
||||
gs-id="{{ widget.id }}"
|
||||
>
|
||||
<div class="card grid-stack-item-content">
|
||||
<div class="card-header text-{{ widget.fg_color }} bg-{{ widget.color|default:"secondary" }} px-2 py-1 d-flex flex-row">
|
||||
<a href="#"
|
||||
hx-get="{% url 'extras:dashboardwidget_config' id=widget.id %}"
|
||||
hx-target="#htmx-modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#htmx-modal"
|
||||
>
|
||||
<i class="mdi mdi-cog text-{{ widget.fg_color }}"></i>
|
||||
</a>
|
||||
<div class="card-title flex-fill text-center">
|
||||
{% if widget.title %}
|
||||
<span class="fs-4 fw-bold">{{ widget.title }}</span>
|
||||
{% endif %}
|
||||
{% with bg_color=widget.color|default:"secondary" %}
|
||||
<div class="card-header text-bg-{{ bg_color }} px-2 py-1 d-flex flex-row">
|
||||
<a href="#"
|
||||
hx-get="{% url 'extras:dashboardwidget_config' id=widget.id %}"
|
||||
hx-target="#htmx-modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#htmx-modal"
|
||||
class="text-bg-{{ bg_color }}"
|
||||
>
|
||||
<i class="mdi mdi-cog"></i>
|
||||
</a>
|
||||
<div class="card-title flex-fill text-center">
|
||||
{% if widget.title %}
|
||||
<span class="fs-4 fw-bold">{{ widget.title }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="#"
|
||||
hx-get="{% url 'extras:dashboardwidget_delete' id=widget.id %}"
|
||||
hx-target="#htmx-modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#htmx-modal"
|
||||
class="text-bg-{{ bg_color }}"
|
||||
>
|
||||
<i class="mdi mdi-close"></i>
|
||||
</a>
|
||||
</div>
|
||||
<a href="#"
|
||||
hx-get="{% url 'extras:dashboardwidget_delete' id=widget.id %}"
|
||||
hx-target="#htmx-modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#htmx-modal"
|
||||
>
|
||||
<i class="mdi mdi-close text-{{ widget.fg_color }}"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-2 pt-1 overflow-auto">
|
||||
{% render_widget widget %}
|
||||
</div>
|
||||
<div class="card-body p-2 pt-1 overflow-auto">
|
||||
{% render_widget widget %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<table class="table table-hover">
|
||||
{% for test, data in tests.items %}
|
||||
<tr>
|
||||
<td class="font-monospace"><a href="#{{ test }}">{{ test }}</a></td>
|
||||
<td class="font-monospace">{{ test }}</td>
|
||||
<td class="text-end report-stats">
|
||||
<span class="badge text-bg-success">{{ data.success }}</span>
|
||||
<span class="badge text-bg-info">{{ data.info }}</span>
|
||||
@@ -41,7 +41,11 @@
|
||||
<div class="card">
|
||||
<div class="table-responsive" id="object_list">
|
||||
<h5 class="card-header">{% trans "Log" %}</h5>
|
||||
{% include 'htmx/table.html' %}
|
||||
<div class="htmx-container table-responsive"
|
||||
hx-get="{% url 'extras:script_result' job_pk=job.pk %}?embedded=True&log=True"
|
||||
hx-target="this"
|
||||
hx-trigger="load" hx-select=".htmx-container" hx-swap="outerHTML"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -90,14 +90,6 @@
|
||||
</div>
|
||||
{# /Object list tab #}
|
||||
|
||||
{# Filters tab #}
|
||||
{% if filter_form %}
|
||||
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
|
||||
{% include 'inc/filter_list.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{# /Filters tab #}
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
|
||||
@@ -81,15 +81,7 @@ Context:
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
<div id="select-all-box" class="d-none card d-print-none">
|
||||
<div class="form col-md-12">
|
||||
<div class="card-body">
|
||||
<div class="float-end">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body d-flex justify-content-between">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
|
||||
<label for="select-all" class="form-check-label">
|
||||
@@ -98,6 +90,14 @@ Context:
|
||||
{% endblocktrans %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="bulk-action-buttons">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,12 +123,14 @@ Context:
|
||||
{# Form buttons #}
|
||||
<div class="btn-list d-print-none mt-2">
|
||||
{% block bulk_buttons %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
<div class="bulk-action-buttons">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{# /Form buttons #}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{# Render an HTMX-enabled table with paginator #}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
<div class="htmx-container table-responsive">
|
||||
@@ -14,5 +15,19 @@
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{# Include the updated object count for display elsewhere on the page #}
|
||||
<div class="d-none" hx-swap-oob="innerHTML:.total-object-count">{{ table.rows|length }}</div>
|
||||
{% if request.htmx %}
|
||||
{# Include the updated object count for display elsewhere on the page #}
|
||||
<div hx-swap-oob="innerHTML:.total-object-count">{{ table.rows|length }}</div>
|
||||
|
||||
{# Update the bulk action buttons with new query parameters #}
|
||||
{% if actions %}
|
||||
<div class="bulk-action-buttons" hx-swap-oob="outerHTML:.bulk-action-buttons">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
<h5 class="card-header">{% trans "Related Objects" %}</h5>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for qs, filter_param in related_models %}
|
||||
{% with viewname=qs.model|viewname:"list" %}
|
||||
{% with viewname=qs.model|validated_viewname:"list" %}
|
||||
{% if viewname is not None %}
|
||||
<a href="{% url viewname %}?{{ filter_param }}={{ object.pk }}" class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ qs.model|meta:"verbose_name_plural"|bettertitle }}
|
||||
{% with count=qs.count %}
|
||||
@@ -16,6 +17,7 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
{% for backend in auth_backends %}
|
||||
<div class="col">
|
||||
<a href="{{ backend.url }}" class="btn w-100">
|
||||
{% if backend.icon_name %}<i class="mdi mdi-{{ backend.icon_name }}"></i>{% endif %}
|
||||
{% if backend.icon_name %}<i class="mdi mdi-{{ backend.icon_name }}"></i>
|
||||
{% elif backend.icon_img %}<img src="{{ backend.icon_img }}" height="24" {% if backend.display_name %}class="me-2" {% endif %}/>{% endif %}
|
||||
{{ backend.display_name }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -3,38 +3,25 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from tenancy import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class TenancyQuery:
|
||||
@strawberry.field
|
||||
def tenant(self, id: int) -> TenantType:
|
||||
return models.Tenant.objects.get(pk=id)
|
||||
tenant: TenantType = strawberry_django.field()
|
||||
tenant_list: List[TenantType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def tenant_group(self, id: int) -> TenantGroupType:
|
||||
return models.TenantGroup.objects.get(pk=id)
|
||||
tenant_group: TenantGroupType = strawberry_django.field()
|
||||
tenant_group_list: List[TenantGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def contact(self, id: int) -> ContactType:
|
||||
return models.Contact.objects.get(pk=id)
|
||||
contact: ContactType = strawberry_django.field()
|
||||
contact_list: List[ContactType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def contact_role(self, id: int) -> ContactRoleType:
|
||||
return models.ContactRole.objects.get(pk=id)
|
||||
contact_role: ContactRoleType = strawberry_django.field()
|
||||
contact_role_list: List[ContactRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def contact_group(self, id: int) -> ContactGroupType:
|
||||
return models.ContactGroup.objects.get(pk=id)
|
||||
contact_group: ContactGroupType = strawberry_django.field()
|
||||
contact_group_list: List[ContactGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def contact_assignment(self, id: int) -> ContactAssignmentType:
|
||||
return models.ContactAssignment.objects.get(pk=id)
|
||||
contact_assignment: ContactAssignmentType = strawberry_django.field()
|
||||
contact_assignment_list: List[ContactAssignmentType] = strawberry_django.field()
|
||||
|
||||
@@ -113,11 +113,12 @@ class ContactAssignmentTable(NetBoxTable):
|
||||
)
|
||||
contact_phone = tables.Column(
|
||||
accessor=Accessor('contact__phone'),
|
||||
verbose_name=_('Contact Phone')
|
||||
verbose_name=_('Contact Phone'),
|
||||
linkify=linkify_phone,
|
||||
)
|
||||
contact_email = tables.Column(
|
||||
contact_email = tables.EmailColumn(
|
||||
accessor=Accessor('contact__email'),
|
||||
verbose_name=_('Contact Email')
|
||||
verbose_name=_('Contact Email'),
|
||||
)
|
||||
contact_address = tables.Column(
|
||||
accessor=Accessor('contact__address'),
|
||||
|
||||
15448
netbox/translations/cs/LC_MESSAGES/django.po
Normal file
15448
netbox/translations/cs/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
15486
netbox/translations/da/LC_MESSAGES/django.po
Normal file
15486
netbox/translations/da/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user