mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-15 11:42:52 -06:00
Fixes #7035: Refactor APISelect query_param logic
This commit is contained in:
parent
0d61dcb1bc
commit
89b7f3f19d
@ -131,10 +131,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region_id'},
|
||||
{'accessor': 'site_group_id', 'field_name': 'site_group_id'}
|
||||
],
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
@ -405,9 +405,9 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'provider_id', 'field_name': 'provider_id'}
|
||||
],
|
||||
label=_('Provider network'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
@ -431,10 +431,10 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region_id'},
|
||||
{'accessor': 'site_group_id', 'field_name': 'site_group_id'},
|
||||
],
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
@ -467,10 +467,10 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
{'accessor': 'site_group_id', 'field_name': 'site_group'},
|
||||
],
|
||||
required=False
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -437,19 +437,19 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
{'accessor': 'group_id', 'field_name': 'site_group'},
|
||||
]
|
||||
)
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
label='VLAN group',
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'site_id', 'field_name': 'site'},
|
||||
],
|
||||
initial_params={
|
||||
'vlans': '$vlan'
|
||||
}
|
||||
@ -458,10 +458,10 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label='VLAN',
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'group_id': '$vlan_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'site_id', 'field_name': 'site'},
|
||||
{'accessor': 'group_id', 'field_name': 'vlan_group'},
|
||||
]
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
@ -573,10 +573,10 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
{'accessor': 'group_id', 'field_name': 'site_group'},
|
||||
],
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
@ -695,9 +695,9 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilter
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region_id'},
|
||||
],
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
@ -883,9 +883,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
interface = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device'
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'device_id', 'field_name': 'device'},
|
||||
],
|
||||
)
|
||||
virtual_machine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
@ -898,9 +898,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
label='Interface',
|
||||
query_params={
|
||||
'virtual_machine_id': '$virtual_machine'
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'virtual_machine_id', 'field_name': 'virtual_machine'},
|
||||
]
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
@ -927,28 +927,28 @@ class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
query_params={
|
||||
'region_id': '$nat_region',
|
||||
'group_id': '$nat_site_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'nat_region'},
|
||||
{'accessor': 'group_id', 'field_name': 'nat_site_group'},
|
||||
],
|
||||
)
|
||||
nat_rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
label='Rack',
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'site_id', 'field_name': 'site'},
|
||||
],
|
||||
)
|
||||
nat_device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label='Device',
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'rack_id': '$nat_rack',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'site_id', 'field_name': 'site'},
|
||||
{'accessor': 'rack_id', 'field_name': 'nat_rack'},
|
||||
]
|
||||
)
|
||||
nat_cluster = DynamicModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
@ -959,9 +959,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
label='Virtual Machine',
|
||||
query_params={
|
||||
'cluster_id': '$nat_cluster',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'cluster_id', 'field_name': 'nat_cluster'},
|
||||
]
|
||||
)
|
||||
nat_vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
@ -972,11 +972,11 @@ class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=IPAddress.objects.all(),
|
||||
required=False,
|
||||
label='IP Address',
|
||||
query_params={
|
||||
'device_id': '$nat_device',
|
||||
'virtual_machine_id': '$nat_virtual_machine',
|
||||
'vrf_id': '$nat_vrf',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'device_id', 'field_name': 'nat_device'},
|
||||
{'accessor': 'virtual_machine_id', 'field_name': 'nat_virtual_machine'},
|
||||
{'accessor': 'vrf_id', 'field_name': 'nat_vrf'},
|
||||
],
|
||||
)
|
||||
primary_for_parent = forms.BooleanField(
|
||||
required=False,
|
||||
@ -1365,10 +1365,10 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
initial_params={
|
||||
'locations': '$location'
|
||||
},
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$sitegroup',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
{'accessor': 'group_id', 'field_name': 'sitegroup'},
|
||||
],
|
||||
)
|
||||
location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@ -1376,17 +1376,17 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
initial_params={
|
||||
'racks': '$rack'
|
||||
},
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'site_id', 'field_name': 'site'},
|
||||
]
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'site_id', 'field_name': 'site'},
|
||||
{'accessor': 'location_id', 'field_name': 'location'},
|
||||
],
|
||||
)
|
||||
clustergroup = DynamicModelChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
@ -1399,9 +1399,9 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
cluster = DynamicModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$clustergroup',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'group_id', 'field_name': 'clustergroup'},
|
||||
],
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
@ -1541,9 +1541,9 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'scope_type': '$scope_type',
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'scope_type', 'field_name': 'scope_type'},
|
||||
],
|
||||
label='VLAN Group'
|
||||
)
|
||||
|
||||
@ -1568,10 +1568,10 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$sitegroup',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
{'accessor': 'group_id', 'field_name': 'sitegroup'},
|
||||
]
|
||||
)
|
||||
|
||||
# Other fields
|
||||
@ -1657,17 +1657,17 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
{'accessor': 'group_id', 'field_name': 'site_group'},
|
||||
]
|
||||
)
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'site_id', 'field_name': 'site'},
|
||||
]
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
@ -1722,9 +1722,9 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region': '$region'
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
],
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
@ -1732,9 +1732,9 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region': '$region'
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
],
|
||||
label=_('VLAN group'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
|
BIN
netbox/project-static/dist/config.js
vendored
BIN
netbox/project-static/dist/config.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/config.js.map
vendored
BIN
netbox/project-static/dist/config.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js
vendored
BIN
netbox/project-static/dist/jobs.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js.map
vendored
BIN
netbox/project-static/dist/jobs.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js
vendored
BIN
netbox/project-static/dist/status.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js.map
vendored
BIN
netbox/project-static/dist/status.js.map
vendored
Binary file not shown.
@ -2,8 +2,9 @@ import queryString from 'query-string';
|
||||
import debounce from 'just-debounce-it';
|
||||
import { readableColor } from 'color2k';
|
||||
import SlimSelect from 'slim-select';
|
||||
import { createToast } from '../bs';
|
||||
import { hasUrl, hasExclusions, isTrigger } from './util';
|
||||
import { createToast } from '../../bs';
|
||||
import { hasUrl, hasExclusions, isTrigger } from '../util';
|
||||
import { FilterFieldMap } from './filterFields';
|
||||
import {
|
||||
isTruthy,
|
||||
hasMore,
|
||||
@ -11,61 +12,14 @@ import {
|
||||
getElement,
|
||||
getApiData,
|
||||
isApiError,
|
||||
getElements,
|
||||
createElement,
|
||||
uniqueByProperty,
|
||||
findFirstAdjacent,
|
||||
} from '../util';
|
||||
} from '../../util';
|
||||
|
||||
import type { Stringifiable } from 'query-string';
|
||||
import type { Option } from 'slim-select/dist/data';
|
||||
|
||||
/**
|
||||
* Map of string keys to primitive array values accepted by `query-string`. Keys are used as
|
||||
* URL query parameter keys. Values correspond to query param values, enforced as an array
|
||||
* for easier handling. For example, a mapping of `{ site_id: [1, 2] }` is serialized by
|
||||
* `query-string` as `?site_id=1&site_id=2`. Likewise, `{ site_id: [1] }` is serialized as
|
||||
* `?site_id=1`.
|
||||
*/
|
||||
type QueryFilter = Map<string, Stringifiable[]>;
|
||||
|
||||
/**
|
||||
* Map of string keys to primitive values. Used to track variables within URLs from the server. For
|
||||
* example, `/api/$key/thing`. `PathFilter` tracks `$key` as `{ key: '' }` in the map, and when the
|
||||
* value is later known, the value is set — `{ key: 'value' }`, and the URL is transformed to
|
||||
* `/api/value/thing`.
|
||||
*/
|
||||
type PathFilter = Map<string, Stringifiable>;
|
||||
|
||||
/**
|
||||
* Merge or replace incoming options with current options.
|
||||
*/
|
||||
type ApplyMethod = 'merge' | 'replace';
|
||||
|
||||
/**
|
||||
* Trigger for which the select instance should fetch its data from the NetBox API.
|
||||
*/
|
||||
export type Trigger =
|
||||
/**
|
||||
* Load data when the select element is opened.
|
||||
*/
|
||||
| 'open'
|
||||
/**
|
||||
* Load data when the element is loaded.
|
||||
*/
|
||||
| 'load'
|
||||
/**
|
||||
* Load data when a parent element is uncollapsed.
|
||||
*/
|
||||
| 'collapse';
|
||||
|
||||
// Various one-off patterns to replace in query param keys.
|
||||
const REPLACE_PATTERNS = [
|
||||
// Don't query `termination_a_device=1`, but rather `device=1`.
|
||||
[new RegExp(/termination_(a|b)_(.+)/g), '$2'],
|
||||
// A tenant's group relationship field is `group`, but the field name is `tenant_group`.
|
||||
[new RegExp(/tenant_(group)/g), '$1'],
|
||||
] as [RegExp, string][];
|
||||
import type { Trigger, PathFilter, ApplyMethod, QueryFilter } from './types';
|
||||
|
||||
// Empty placeholder option.
|
||||
const PLACEHOLDER = {
|
||||
@ -81,7 +35,7 @@ const DISABLED_ATTRIBUTES = ['occupied'] as string[];
|
||||
* Manage a single API-backed select element's state. Each API select element is likely controlled
|
||||
* or dynamically updated by one or more other API select (or static select) elements' values.
|
||||
*/
|
||||
class APISelect {
|
||||
export class APISelect {
|
||||
/**
|
||||
* Base `<select/>` DOM element.
|
||||
*/
|
||||
@ -124,24 +78,29 @@ class APISelect {
|
||||
*/
|
||||
private readonly slim: InstanceType<typeof SlimSelect>;
|
||||
|
||||
/**
|
||||
* API query parameters that should be applied to API queries for this field. This will be
|
||||
* updated as other dependent fields' values change. This is a mapping of:
|
||||
*
|
||||
* Form Field Names → Form Field Values
|
||||
*
|
||||
* This is/might be different than the query parameters themselves, as the form field names may
|
||||
* be different than the object model key names. For example, `tenant_group` would be the field
|
||||
* name, but `group` would be the query parameter. Query parameters themselves are tracked in
|
||||
* `queryParams`.
|
||||
*/
|
||||
private readonly filterParams: QueryFilter = new Map();
|
||||
|
||||
/**
|
||||
* Post-parsed URL query parameters for API queries.
|
||||
*/
|
||||
private readonly queryParams: QueryFilter = new Map();
|
||||
|
||||
/**
|
||||
* API query parameters that should be applied to API queries for this field. This will be
|
||||
* updated as other dependent fields' values change. This is a mapping of:
|
||||
*
|
||||
* Form Field Names → Object containing:
|
||||
* - Query parameter key name
|
||||
* - Query value
|
||||
* - Other options such as a default value, and the option to include
|
||||
* null values.
|
||||
*
|
||||
* This is different from `queryParams` in that it tracks all _possible_ related fields and their
|
||||
* values, even if they are empty. Further, the keys in `queryParams` correspond to the actual
|
||||
* query parameter keys, which are not necessarily the same as the form field names, depending on
|
||||
* the model. For example, `tenant_group` would be the field name, but `group_id` would be the
|
||||
* query parameter.
|
||||
*/
|
||||
private readonly filterFields: FilterFieldMap = new FilterFieldMap();
|
||||
|
||||
/**
|
||||
* Mapping of URL template key/value pairs. If this element's URL contains Django template tags
|
||||
* (e.g., `{{key}}`), `key` will be added to `pathValue` and the `id_key` form element will be
|
||||
@ -228,17 +187,27 @@ class APISelect {
|
||||
});
|
||||
|
||||
// Initialize API query properties.
|
||||
this.getFilteredBy();
|
||||
// this.getFilteredBy();
|
||||
this.getFilterFields();
|
||||
this.getPathKeys();
|
||||
|
||||
for (const filter of this.filterParams.keys()) {
|
||||
// for (const filter of this.filterParams.keys()) {
|
||||
// this.updateQueryParams(filter);
|
||||
// }
|
||||
for (const filter of this.filterFields.keys()) {
|
||||
this.updateQueryParams(filter);
|
||||
}
|
||||
|
||||
// Add any already-resolved key/value pairs to the API query parameters.
|
||||
for (const [key, value] of this.filterParams.entries()) {
|
||||
if (isTruthy(value)) {
|
||||
this.queryParams.set(key, value);
|
||||
// for (const [key, value] of this.filterParams.entries()) {
|
||||
// if (isTruthy(value)) {
|
||||
// this.queryParams.set(key, value);
|
||||
// }
|
||||
// }
|
||||
for (const value of this.filterFields.values()) {
|
||||
const { queryParam, queryValue } = value;
|
||||
if (isTruthy(queryValue)) {
|
||||
this.queryParams.set(queryParam, queryValue);
|
||||
}
|
||||
}
|
||||
|
||||
@ -395,7 +364,8 @@ class APISelect {
|
||||
|
||||
// Create a unique iterator of all possible form fields which, when changed, should cause this
|
||||
// element to update its API query.
|
||||
const dependencies = new Set([...this.filterParams.keys(), ...this.pathValues.keys()]);
|
||||
// const dependencies = new Set([...this.filterParams.keys(), ...this.pathValues.keys()]);
|
||||
const dependencies = new Set([...this.filterFields.keys(), ...this.pathValues.keys()]);
|
||||
|
||||
for (const dep of dependencies) {
|
||||
const filterElement = document.querySelector(`[name="${dep}"]`);
|
||||
@ -588,6 +558,12 @@ class APISelect {
|
||||
this.updateQueryParams(target.name);
|
||||
this.updatePathValues(target.name);
|
||||
this.updateQueryUrl();
|
||||
|
||||
console.group(this.name, this.queryUrl);
|
||||
console.log(this.filterFields);
|
||||
console.log(this.queryParams);
|
||||
console.groupEnd();
|
||||
|
||||
// Load new data.
|
||||
Promise.all([this.loadData()]);
|
||||
}
|
||||
@ -655,27 +631,12 @@ class APISelect {
|
||||
* Update an element's API URL based on the value of another element on which this element
|
||||
* relies.
|
||||
*
|
||||
* @param id DOM ID of the other element.
|
||||
* @param fieldName DOM ID of the other element.
|
||||
*/
|
||||
private updateQueryParams(id: string): void {
|
||||
let key = id.replaceAll(/^id_/gi, '');
|
||||
private updateQueryParams(fieldName: string): void {
|
||||
// Find the element dependency.
|
||||
const element = getElement<HTMLSelectElement>(`id_${key}`);
|
||||
const element = document.querySelector<HTMLSelectElement>(`[name="${fieldName}"]`);
|
||||
if (element !== null) {
|
||||
// If the dependency has a value, parse the dependency's name (form key) for any
|
||||
// required replacements.
|
||||
for (const [pattern, replacement] of REPLACE_PATTERNS) {
|
||||
if (key.match(pattern)) {
|
||||
key = key.replaceAll(pattern, replacement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force related keys to end in `_id`, if they don't already.
|
||||
if (key.substring(key.length - 3) !== '_id') {
|
||||
key = `${key}_id`;
|
||||
}
|
||||
|
||||
// Initialize the element value as an array, in case there are multiple values.
|
||||
let elementValue = [] as Stringifiable[];
|
||||
|
||||
@ -694,13 +655,29 @@ class APISelect {
|
||||
|
||||
if (elementValue.length > 0) {
|
||||
// If the field has a value, add it to the map.
|
||||
if (this.filterParams.has(id)) {
|
||||
// If this instance is filtered by the neighbor element, add its value to the map.
|
||||
this.queryParams.set(key, elementValue);
|
||||
this.filterFields.updateValue(fieldName, elementValue);
|
||||
|
||||
const current = this.filterFields.get(fieldName);
|
||||
|
||||
if (typeof current !== 'undefined') {
|
||||
const { queryParam, queryValue, includeNull } = current;
|
||||
let value = [] as Stringifiable[];
|
||||
if (includeNull) {
|
||||
value = [...value, null];
|
||||
}
|
||||
if (queryValue.length > 0) {
|
||||
value = [...value, ...queryValue];
|
||||
this.queryParams.set(queryParam, value);
|
||||
} else {
|
||||
this.queryParams.delete(queryParam);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise, delete it (we don't want to send an empty query like `?site_id=`)
|
||||
this.queryParams.delete(key);
|
||||
const queryParam = this.filterFields.queryParam(fieldName);
|
||||
if (queryParam !== null) {
|
||||
this.queryParams.delete(queryParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -798,86 +775,17 @@ class APISelect {
|
||||
/**
|
||||
* Determine if a select element should be filtered by the value of another select element.
|
||||
*
|
||||
* Looks for the DOM attribute `data-query-param-<name of other field>`, which would look like:
|
||||
* `["$<name>"]`
|
||||
*
|
||||
* If the attribute exists, parse out the raw value. In the above example, this would be `name`.
|
||||
* Looks for the DOM attribute `data-filter-fields`, the value of which is a JSON array of
|
||||
* objects containing information about how to handle the related field.
|
||||
*/
|
||||
private getFilteredBy(): void {
|
||||
const pattern = new RegExp(/\[|\]|"|\$/g);
|
||||
const keyPattern = new RegExp(/data-query-param-/g);
|
||||
|
||||
// Extract data attributes.
|
||||
const keys = Object.values(this.base.attributes)
|
||||
.map(v => v.name)
|
||||
.filter(v => v.includes('data'));
|
||||
|
||||
/**
|
||||
* Properly handle preexistence of keys, value types, and deduplication when adding a filter to
|
||||
* `filterParams`.
|
||||
*
|
||||
* _Note: This is an unnamed function so that it can access `this`._
|
||||
*/
|
||||
const addFilter = (key: string, value: Stringifiable): void => {
|
||||
const current = this.filterParams.get(key);
|
||||
|
||||
if (typeof current !== 'undefined') {
|
||||
// This instance is already filtered by `key`, so we should add the new `value`.
|
||||
// Merge and deduplicate the current filter parameter values with the incoming value.
|
||||
const next = Array.from(
|
||||
new Set<Stringifiable>([...(current as Stringifiable[]), value]),
|
||||
);
|
||||
this.filterParams.set(key, next);
|
||||
} else {
|
||||
// This instance is not already filtered by `key`, so we should add a new mapping.
|
||||
if (value === '') {
|
||||
// Don't add placeholder values.
|
||||
this.filterParams.set(key, []);
|
||||
} else {
|
||||
// If the value is not a placeholder, add it.
|
||||
this.filterParams.set(key, [value]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const key of keys) {
|
||||
if (key.match(keyPattern) && key !== 'data-query-param-exclude') {
|
||||
const value = this.base.getAttribute(key);
|
||||
if (value !== null) {
|
||||
try {
|
||||
const parsed = JSON.parse(value) as string | string[];
|
||||
if (Array.isArray(parsed)) {
|
||||
// Query param contains multiple values.
|
||||
for (const item of parsed) {
|
||||
if (item.match(/^\$.+$/g)) {
|
||||
// Value is an unfulfilled variable.
|
||||
addFilter(item.replaceAll(pattern, ''), '');
|
||||
} else {
|
||||
// Value has been fulfilled and is a real value to query.
|
||||
addFilter(key.replaceAll(keyPattern, ''), item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (parsed.match(/^\$.+$/g)) {
|
||||
// Value is an unfulfilled variable.
|
||||
addFilter(parsed.replaceAll(pattern, ''), '');
|
||||
} else {
|
||||
// Value has been fulfilled and is a real value to query.
|
||||
addFilter(key.replaceAll(keyPattern, ''), parsed);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
if (value.match(/^\$.+$/g)) {
|
||||
// Value is an unfulfilled variable.
|
||||
addFilter(value.replaceAll(pattern, ''), '');
|
||||
} else {
|
||||
// Value has been fulfilled and is a real value to query.
|
||||
addFilter(key.replaceAll(keyPattern, ''), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private getFilterFields(): void {
|
||||
const serialized = this.base.getAttribute('data-filter-fields');
|
||||
try {
|
||||
this.filterFields.addFromJson(serialized);
|
||||
} catch (err) {
|
||||
console.group(`Unable to determine filter fields for select field '${this.name}'`);
|
||||
console.warn(err);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@ -990,9 +898,3 @@ class APISelect {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initApiSelect(): void {
|
||||
for (const select of getElements<HTMLSelectElement>('.netbox-api-select')) {
|
||||
new APISelect(select);
|
||||
}
|
||||
}
|
87
netbox/project-static/src/select/api/filterFields.ts
Normal file
87
netbox/project-static/src/select/api/filterFields.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { isTruthy } from '../../util';
|
||||
import { isDataFilterFields } from './types';
|
||||
|
||||
import type { Stringifiable } from 'query-string';
|
||||
import type { FilterFieldValue } from './types';
|
||||
|
||||
/**
|
||||
* Extension of built-in `Map` to add convenience functions.
|
||||
*/
|
||||
export class FilterFieldMap extends Map<string, FilterFieldValue> {
|
||||
/**
|
||||
* Get the query parameter key based on field name.
|
||||
*
|
||||
* @param fieldName Related field name.
|
||||
* @returns `queryParam` key.
|
||||
*/
|
||||
public queryParam(fieldName: string): Nullable<FilterFieldValue['queryParam']> {
|
||||
const value = this.get(fieldName);
|
||||
if (typeof value !== 'undefined') {
|
||||
return value.queryParam;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query parameter value based on field name.
|
||||
*
|
||||
* @param fieldName Related field name.
|
||||
* @returns `queryValue` value, or an empty array if there is no corresponding Map entry.
|
||||
*/
|
||||
public queryValue(fieldName: string): FilterFieldValue['queryValue'] {
|
||||
const value = this.get(fieldName);
|
||||
if (typeof value !== 'undefined') {
|
||||
return value.queryValue;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the value of a field when the value changes.
|
||||
*
|
||||
* @param fieldName Related field name.
|
||||
* @param queryValue New value.
|
||||
* @returns `true` if the update was successful, `false` if there was no corresponding Map entry.
|
||||
*/
|
||||
public updateValue(fieldName: string, queryValue: FilterFieldValue['queryValue']): boolean {
|
||||
const current = this.get(fieldName);
|
||||
if (isTruthy(current)) {
|
||||
const { queryParam, includeNull } = current;
|
||||
this.set(fieldName, { queryParam, queryValue, includeNull });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the underlying map based on the JSON passed in the `data-filter-fields` attribute.
|
||||
*
|
||||
* @param json Raw JSON string from `data-filter-fields` attribute.
|
||||
*/
|
||||
public addFromJson(json: string | null | undefined): void {
|
||||
if (isTruthy(json)) {
|
||||
const deserialized = JSON.parse(json);
|
||||
// Ensure the value is the data structure we expect.
|
||||
if (isDataFilterFields(deserialized)) {
|
||||
for (const { queryParam, fieldName, defaultValue, includeNull } of deserialized) {
|
||||
let queryValue = [] as Stringifiable[];
|
||||
if (isTruthy(defaultValue)) {
|
||||
// Add the default value, if it exists.
|
||||
if (Array.isArray(defaultValue)) {
|
||||
// If the default value is an array, add all elements to the value.
|
||||
queryValue = [...queryValue, ...defaultValue];
|
||||
} else {
|
||||
queryValue = [defaultValue];
|
||||
}
|
||||
}
|
||||
// Populate the underlying map with the initial data.
|
||||
this.set(fieldName, { queryParam, queryValue, includeNull });
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Data from 'data-filter-fields' attribute is improperly formatted: '${json}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
netbox/project-static/src/select/api/index.ts
Normal file
10
netbox/project-static/src/select/api/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { getElements } from '../../util';
|
||||
import { APISelect } from './apiSelect';
|
||||
|
||||
export function initApiSelect(): void {
|
||||
for (const select of getElements<HTMLSelectElement>('.netbox-api-select')) {
|
||||
new APISelect(select);
|
||||
}
|
||||
}
|
||||
|
||||
export type { Trigger } from './types';
|
111
netbox/project-static/src/select/api/types.ts
Normal file
111
netbox/project-static/src/select/api/types.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import type { Stringifiable } from 'query-string';
|
||||
|
||||
/**
|
||||
* Map of string keys to primitive array values accepted by `query-string`. Keys are used as
|
||||
* URL query parameter keys. Values correspond to query param values, enforced as an array
|
||||
* for easier handling. For example, a mapping of `{ site_id: [1, 2] }` is serialized by
|
||||
* `query-string` as `?site_id=1&site_id=2`. Likewise, `{ site_id: [1] }` is serialized as
|
||||
* `?site_id=1`.
|
||||
*/
|
||||
export type QueryFilter = Map<string, Stringifiable[]>;
|
||||
|
||||
/**
|
||||
* Tracked data for a related field. This is the value of `APISelect.filterFields`.
|
||||
*/
|
||||
export type FilterFieldValue = {
|
||||
/**
|
||||
* Key to use in the query parameter itself.
|
||||
*/
|
||||
queryParam: string;
|
||||
/**
|
||||
* Value to use in the query parameter for the related field.
|
||||
*/
|
||||
queryValue: Stringifiable[];
|
||||
/**
|
||||
* @see `DataFilterFields.includeNull`
|
||||
*/
|
||||
includeNull: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* JSON data passed from Django on the `data-filter-fields` attribute.
|
||||
*/
|
||||
export type DataFilterFields = {
|
||||
/**
|
||||
* Related field form name (`[name="<fieldName>"]`)
|
||||
*
|
||||
* @example tenant_group
|
||||
*/
|
||||
fieldName: string;
|
||||
/**
|
||||
* Key to use in the query parameter itself.
|
||||
*
|
||||
* @example group_id
|
||||
*/
|
||||
queryParam: string;
|
||||
/**
|
||||
* Optional default value. If set, value will be added to the query parameters prior to the
|
||||
* initial API call and will be maintained until the field `fieldName` references (if one exists)
|
||||
* is updated with a new value.
|
||||
*
|
||||
* @example 1
|
||||
*/
|
||||
defaultValue: Nullable<Stringifiable | Stringifiable[]>;
|
||||
/**
|
||||
* Include `null` on queries for the related field. For example, if `true`, `?<fieldName>=null`
|
||||
* will be added to all API queries for this field.
|
||||
*/
|
||||
includeNull: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map of string keys to primitive values. Used to track variables within URLs from the server. For
|
||||
* example, `/api/$key/thing`. `PathFilter` tracks `$key` as `{ key: '' }` in the map, and when the
|
||||
* value is later known, the value is set — `{ key: 'value' }`, and the URL is transformed to
|
||||
* `/api/value/thing`.
|
||||
*/
|
||||
export type PathFilter = Map<string, Stringifiable>;
|
||||
|
||||
/**
|
||||
* Merge or replace incoming options with current options.
|
||||
*/
|
||||
export type ApplyMethod = 'merge' | 'replace';
|
||||
|
||||
/**
|
||||
* Trigger for which the select instance should fetch its data from the NetBox API.
|
||||
*/
|
||||
export type Trigger =
|
||||
/**
|
||||
* Load data when the select element is opened.
|
||||
*/
|
||||
| 'open'
|
||||
/**
|
||||
* Load data when the element is loaded.
|
||||
*/
|
||||
| 'load'
|
||||
/**
|
||||
* Load data when a parent element is uncollapsed.
|
||||
*/
|
||||
| 'collapse';
|
||||
|
||||
/**
|
||||
* Strict Type Guard to determine if a deserialized value from the `data-filter-fields` attribute
|
||||
* is of type `DataFilterFields`.
|
||||
*
|
||||
* @param value Deserialized value from `data-filter-fields` attribute.
|
||||
*/
|
||||
export function isDataFilterFields(value: unknown): value is DataFilterFields[] {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
if ('fieldName' in item && 'queryParam' in item) {
|
||||
return (
|
||||
typeof (item as DataFilterFields).fieldName === 'string' &&
|
||||
typeof (item as DataFilterFields).queryParam === 'string'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
@ -39,6 +39,8 @@ export function isTruthy<V extends unknown>(value: V): value is NonNullable<V> {
|
||||
return true;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return true;
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -170,9 +170,9 @@ class TenancyForm(forms.Form):
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$tenant_group'
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'group_id', 'field_name': 'tenant_group'}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@ -188,9 +188,9 @@ class TenancyFilterForm(forms.Form):
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'group_id': '$tenant_group_id'
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'group_id', 'field_name': 'tenant_group'}
|
||||
],
|
||||
label=_('Tenant'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
|
@ -371,17 +371,20 @@ class DynamicModelChoiceMixin:
|
||||
choice (optional)
|
||||
:param str fetch_trigger: The event type which will cause the select element to
|
||||
fetch data from the API. Must be 'load', 'open', or 'collapse'. (optional)
|
||||
:param filter_fields: A dictionary or list of dictionaries that define a related
|
||||
field. Example: `{'accessor': 'group_id', 'field_name': 'tenant_group'}` (optional)
|
||||
"""
|
||||
filter = django_filters.ModelChoiceFilter
|
||||
widget = widgets.APISelect
|
||||
|
||||
def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, fetch_trigger=None, *args,
|
||||
**kwargs):
|
||||
def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, fetch_trigger=None,
|
||||
filter_fields=[], *args, **kwargs):
|
||||
self.query_params = query_params or {}
|
||||
self.initial_params = initial_params or {}
|
||||
self.null_option = null_option
|
||||
self.disabled_indicator = disabled_indicator
|
||||
self.fetch_trigger = fetch_trigger
|
||||
self.filter_fields = filter_fields
|
||||
|
||||
# to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
|
||||
# by widget_attrs()
|
||||
@ -412,6 +415,10 @@ class DynamicModelChoiceMixin:
|
||||
for key, value in self.query_params.items():
|
||||
widget.add_query_param(key, value)
|
||||
|
||||
# Attach any dynamic query parameters
|
||||
if len(self.filter_fields) > 0:
|
||||
widget.add_filter_fields(self.filter_fields)
|
||||
|
||||
return attrs
|
||||
|
||||
def get_bound_field(self, form, field_name):
|
||||
|
@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Dict, Sequence, Union
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@ -26,6 +27,8 @@ __all__ = (
|
||||
'TimePicker',
|
||||
)
|
||||
|
||||
JSONPrimitive = Union[str, bool, int, float, None]
|
||||
|
||||
|
||||
class SmallTextarea(forms.Textarea):
|
||||
"""
|
||||
@ -142,22 +145,81 @@ class APISelect(SelectWithDisabled):
|
||||
if api_url:
|
||||
self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/')) # Inject BASE_PATH
|
||||
|
||||
def add_query_param(self, name, value):
|
||||
def add_query_param(self, key: str, value: JSONPrimitive) -> None:
|
||||
"""
|
||||
Add details for an additional query param in the form of a data-* JSON-encoded list attribute.
|
||||
|
||||
:param name: The name of the query param
|
||||
:param value: The value of the query param
|
||||
Add a query parameter with a static value to the API request.
|
||||
"""
|
||||
key = f'data-query-param-{name}'
|
||||
self.add_filter_fields({'accessor': key, 'field_name': key, 'default_value': value})
|
||||
|
||||
values = json.loads(self.attrs.get(key, '[]'))
|
||||
if type(value) in (list, tuple):
|
||||
values.extend([str(v) for v in value])
|
||||
else:
|
||||
values.append(str(value))
|
||||
def add_filter_fields(self, filter_fields: Union[Dict[str, JSONPrimitive], Sequence[Dict[str, JSONPrimitive]]]) -> None:
|
||||
"""
|
||||
Add details about another form field, the value for which should
|
||||
be added to this APISelect's URL query parameters.
|
||||
|
||||
self.attrs[key] = json.dumps(values)
|
||||
:Example:
|
||||
|
||||
```python
|
||||
{
|
||||
'field_name': 'tenant_group',
|
||||
'accessor': 'tenant',
|
||||
'default_value': 1,
|
||||
'include_null': False,
|
||||
}
|
||||
```
|
||||
|
||||
:param filter_fields: Dict or list of dicts with the following properties:
|
||||
|
||||
- accessor: The related field's property name. For example, on the
|
||||
`Tenant`model, a related model might be `TenantGroup`. In
|
||||
this case, `accessor` would be `group_id`.
|
||||
|
||||
- field_name: The related field's form name. In the above `Tenant`
|
||||
example, `field_name` would be `tenant_group`.
|
||||
|
||||
- default_value: (Optional) Set a default initial value, which can be
|
||||
overridden if the field changes.
|
||||
|
||||
- include_null: (Optional) Include `null` on queries for the related
|
||||
field. For example, if `True`, `?<fieldName>=null` will
|
||||
be added to all API queries for this field.
|
||||
|
||||
"""
|
||||
key = 'data-filter-fields'
|
||||
# Deserialize the current serialized value from the widget, using an empty JSON
|
||||
# array as a fallback in the event one is not defined.
|
||||
current = json.loads(self.attrs.get(key, '[]'))
|
||||
|
||||
# Create a new list of filter fields using camelCse to align with front-end code standards
|
||||
# (this value will be read and used heavily at the JavaScript layer).
|
||||
update: Sequence[Dict[str, str]] = []
|
||||
try:
|
||||
if isinstance(filter_fields, Sequence):
|
||||
update = [
|
||||
{
|
||||
'fieldName': field['field_name'],
|
||||
'queryParam': field['accessor'],
|
||||
'defaultValue': field.get('default_value'),
|
||||
'includeNull': field.get('include_null', False),
|
||||
} for field in filter_fields
|
||||
]
|
||||
elif isinstance(filter_fields, Dict):
|
||||
update = [
|
||||
{
|
||||
'fieldName': filter_fields['field_name'],
|
||||
'queryParam': filter_fields['accessor'],
|
||||
'defaultValue': filter_fields.get('default_value'),
|
||||
'includeNull': filter_fields.get('include_null', False),
|
||||
}
|
||||
]
|
||||
|
||||
except KeyError as error:
|
||||
raise KeyError(f"Missing required property '{error.args[0]}' on APISelect.filter_fields") from error
|
||||
|
||||
# Combine the current values with the updated values and serialize the result as
|
||||
# JSON. Note: the `separators` kwarg effectively removes extra whitespace from
|
||||
# the serialized JSON string, which is ideal since these will be passed as
|
||||
# attributes to HTML elements and parsed on the client.
|
||||
self.attrs[key] = json.dumps([*current, *update], separators=(',', ':'))
|
||||
|
||||
|
||||
class APISelectMultiple(APISelect, forms.SelectMultiple):
|
||||
|
@ -126,10 +126,10 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
{'accessor': 'group_id', 'field_name': 'site_group'},
|
||||
],
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
@ -206,10 +206,10 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBul
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
{'accessor': 'group_id', 'field_name': 'site_group'},
|
||||
]
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
@ -260,10 +260,10 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region_id'},
|
||||
{'accessor': 'site_group_id', 'field_name': 'site_group'},
|
||||
],
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
@ -291,26 +291,26 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region'},
|
||||
{'accessor': 'group_id', 'field_name': 'site_group'},
|
||||
]
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'site_id', 'field_name': 'site'},
|
||||
]
|
||||
)
|
||||
devices = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'rack_id': '$rack',
|
||||
'cluster_id': 'null',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'site_id', 'field_name': 'site'},
|
||||
{'accessor': 'rack_id', 'field_name': 'rack'},
|
||||
{'accessor': 'cluster_id', 'field_name': 'cluster', 'default_value': None},
|
||||
]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -362,16 +362,16 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
)
|
||||
cluster = DynamicModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
query_params={
|
||||
'group_id': '$cluster_group'
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'group_id', 'field_name': 'cluster_group'},
|
||||
]
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
"vm_role": "True"
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'vm_role', 'field_name': 'vm_role', 'default_value': True},
|
||||
],
|
||||
)
|
||||
platform = DynamicModelChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
@ -510,9 +510,9 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldM
|
||||
vm_role=True
|
||||
),
|
||||
required=False,
|
||||
query_params={
|
||||
"vm_role": "True"
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'vm_role', 'field_name': 'vm_role', 'default_value': True},
|
||||
]
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
@ -595,10 +595,10 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'region_id', 'field_name': 'region_id'},
|
||||
{'accessor': 'group_id', 'field_name': 'site_group_id'},
|
||||
],
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
@ -606,9 +606,9 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'vm_role': "True"
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'vm_role', 'field_name': 'vm_role', 'default_value': True},
|
||||
],
|
||||
label=_('Role'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
@ -657,17 +657,17 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label='Untagged VLAN',
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'group_id', 'field_name': 'vlan_group'},
|
||||
]
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label='Tagged VLANs',
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'group_id', 'field_name': 'vlan_group'},
|
||||
]
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
@ -718,9 +718,9 @@ class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonFo
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'virtual_machine_id': '$virtual_machine',
|
||||
}
|
||||
filter_fields=[
|
||||
{'accessor': 'virtual_machine_id', 'field_name': 'virtual_machine'},
|
||||
]
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
@ -896,9 +896,9 @@ class VMInterfaceFilterForm(BootstrapMixin, forms.Form):
|
||||
virtual_machine_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'cluster_id': '$cluster_id'
|
||||
},
|
||||
filter_fields=[
|
||||
{'accessor': 'cluster_id', 'field_name': 'cluster_id'},
|
||||
],
|
||||
label=_('Virtual machine'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user