From 3624b88c3fa6cb677756823db1413081b3a6607a Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Thu, 8 Jan 2026 21:33:06 +0100 Subject: [PATCH 01/10] Closes #21035: Add .gitkeep to track the media directory (#21074) --- .gitignore | 3 ++- netbox/media/.gitkeep | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 netbox/media/.gitkeep diff --git a/.gitignore b/.gitignore index eb1eccbef..ea2ce9512 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,8 @@ yarn-error.log* /netbox/netbox/configuration.py /netbox/netbox/ldap_config.py /netbox/local/* -/netbox/media +/netbox/media/* +!/netbox/media/.gitkeep /netbox/reports/* !/netbox/reports/__init__.py /netbox/scripts/* diff --git a/netbox/media/.gitkeep b/netbox/media/.gitkeep new file mode 100644 index 000000000..e69de29bb From c11f4b3716d7f02787568121bf7e3e90d03a22f4 Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 12 Jan 2026 09:00:03 +0100 Subject: [PATCH 02/10] 21075-rename-l2vpn-terminations-menu-entry --- netbox/netbox/navigation/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 34b66ada0..052200f47 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -232,7 +232,7 @@ VPN_MENU = Menu( label=_('L2VPNs'), items=( get_model_item('vpn', 'l2vpn', _('L2VPNs')), - get_model_item('vpn', 'l2vpntermination', _('Terminations')), + get_model_item('vpn', 'l2vpntermination', _('L2VPN Terminations')), ), ), MenuGroup( From c3e111c769084ed1aa312bbce3f50fc0b9f16843 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 12 Jan 2026 14:34:17 -0500 Subject: [PATCH 03/10] Fixes #21102: Fix GraphiQL explorer UI --- netbox/project-static/dist/graphiql/index.umd.js | 2 +- .../dist/graphiql/plugin-explorer-style.css | 2 +- netbox/project-static/netbox-graphiql/package.json | 2 +- netbox/project-static/yarn.lock | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox/project-static/dist/graphiql/index.umd.js b/netbox/project-static/dist/graphiql/index.umd.js index 5b7ff2e20..5474017e4 100644 --- a/netbox/project-static/dist/graphiql/index.umd.js +++ b/netbox/project-static/dist/graphiql/index.umd.js @@ -1 +1 @@ -(function(z,P){typeof exports=="object"&&typeof module<"u"?P(exports,require("react"),require("@graphiql/react"),require("graphql")):typeof define=="function"&&define.amd?define(["exports","react","@graphiql/react","graphql"],P):(z=typeof globalThis<"u"?globalThis:z||self,P(z.GraphiQLPluginExplorer={},z.React,z.GraphiQL.React,z.GraphiQL.GraphQL))})(this,function(z,P,X,Ne){"use strict";function ve(i){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(i){for(const l in i)if(l!=="default"){const n=Object.getOwnPropertyDescriptor(i,l);Object.defineProperty(t,l,n.get?n:{enumerable:!0,get:()=>i[l]})}}return t.default=i,Object.freeze(t)}const N=ve(P),Me=ve(Ne);function ye(i){return i&&Object.prototype.hasOwnProperty.call(i,"default")&&Object.keys(i).length===1?i.default:i}var ie={},ae={};const Le=ye(N),je=ye(Me);Object.defineProperty(ae,"__esModule",{value:!0});var Pe=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(i){return typeof i}:function(i){return i&&typeof Symbol=="function"&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i},he=function(){function i(t,l){var n=[],e=!0,s=!1,c=void 0;try{for(var f=t[Symbol.iterator](),u;!(e=(u=f.next()).done)&&(n.push(u.value),!(l&&n.length===l));e=!0);}catch(r){s=!0,c=r}finally{try{!e&&f.return&&f.return()}finally{if(s)throw c}}return n}return function(t,l){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return i(t,l);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),k=Object.assign||function(i){for(var t=1;t"u"?p=!0:typeof r.kind=="string"&&(h=!0)}catch{}var F=e.props.selection,d=e._getArgSelection();if(!d){console.error("missing arg selection when setting arg value");return}var b=Q(e.props.arg.type),v=(0,g.isLeafType)(b)||m||p||h;if(!v){console.warn("Unable to handle non leaf types in InputArgView.setArgValue",r);return}var E=void 0,_=void 0;r===null||typeof r>"u"?_=null:!r.target&&r.kind&&r.kind==="VariableDefinition"?(E=r,_=E.variable):typeof r.kind=="string"?_=r:r.target&&typeof r.target.value=="string"&&(E=r.target.value,_=Ee(b,E));var A=e.props.modifyFields((F.fields||[]).map(function(D){var U=D===d,j=U?k({},D,{value:_}):D;return j}),o);return A},e._modifyChildFields=function(r){return e.props.modifyFields(e.props.selection.fields.map(function(o){return o.name.value===e.props.arg.name?k({},o,{value:{kind:"ObjectValue",fields:r}}):o}),!0)},n),O(e,s)}return I(t,[{key:"render",value:function(){var n=this.props,e=n.arg,s=n.parentField,c=this._getArgSelection();return a.createElement(_e,{argValue:c?c.value:null,arg:e,parentField:s,addArg:this._addArg,removeArg:this._removeArg,setArgFields:this._modifyChildFields,setArgValue:this._setArgValue,getDefaultScalarArgValue:this.props.getDefaultScalarArgValue,makeDefaultArg:this.props.makeDefaultArg,onRunOperation:this.props.onRunOperation,styleConfig:this.props.styleConfig,onCommit:this.props.onCommit,definition:this.props.definition})}}]),t}(a.PureComponent);function pe(i){if((0,g.isEnumType)(i))return{kind:"EnumValue",value:i.getValues()[0].name};switch(i.name){case"String":return{kind:"StringValue",value:""};case"Float":return{kind:"FloatValue",value:"1.5"};case"Int":return{kind:"IntValue",value:"10"};case"Boolean":return{kind:"BooleanValue",value:!1};default:return{kind:"StringValue",value:""}}}function Ce(i,t,l){return pe(l)}var Qe=function(i){B(t,i);function t(){var l,n,e,s;q(this,t);for(var c=arguments.length,f=Array(c),u=0;u"u"?p=!0:typeof r.kind=="string"&&(h=!0)}catch{}var F=e.props.selection,d=e._getArgSelection();if(!d&&!m){console.error("missing arg selection when setting arg value");return}var b=Q(e.props.arg.type),v=(0,g.isLeafType)(b)||m||p||h;if(!v){console.warn("Unable to handle non leaf types in ArgView._setArgValue");return}var E=void 0,_=void 0;return r===null||typeof r>"u"?_=null:r.target&&typeof r.target.value=="string"?(E=r.target.value,_=Ee(b,E)):!r.target&&r.kind==="VariableDefinition"?(E=r,_=E.variable):typeof r.kind=="string"&&(_=r),e.props.modifyArguments((F.arguments||[]).map(function(A){return A===d?k({},A,{value:_}):A}),o)},e._setArgFields=function(r,o){var p=e.props.selection,m=e._getArgSelection();if(!m){console.error("missing arg selection when setting arg value");return}return e.props.modifyArguments((p.arguments||[]).map(function(h){return h===m?k({},h,{value:{kind:"ObjectValue",fields:r}}):h}),o)},n),O(e,s)}return I(t,[{key:"render",value:function(){var n=this.props,e=n.arg,s=n.parentField,c=this._getArgSelection();return a.createElement(_e,{argValue:c?c.value:null,arg:e,parentField:s,addArg:this._addArg,removeArg:this._removeArg,setArgFields:this._setArgFields,setArgValue:this._setArgValue,getDefaultScalarArgValue:this.props.getDefaultScalarArgValue,makeDefaultArg:this.props.makeDefaultArg,onRunOperation:this.props.onRunOperation,styleConfig:this.props.styleConfig,onCommit:this.props.onCommit,definition:this.props.definition})}}]),t}(a.PureComponent);function Ze(i){return i.ctrlKey&&i.key==="Enter"}function Ke(i){return i!=="FragmentDefinition"}var $e=function(i){B(t,i);function t(){var l,n,e,s;q(this,t);for(var c=arguments.length,f=Array(c),u=0;u0?E=""+b+v:E=b;var _=c.type.toString(),A=(0,g.parseType)(_),D={kind:"VariableDefinition",variable:{kind:"Variable",name:{kind:"Name",value:E}},type:A,directives:[]},U=function(x){return(n.props.definition.variableDefinitions||[]).find(function(T){return T.variable.name.value===x})},j=void 0,W={};if(typeof s<"u"&&s!==null){var H=(0,g.visit)(s,{Variable:function(x){var T=x.name.value,$=U(T);if(W[T]=W[T]+1||1,!!$)return $.defaultValue}}),K=D.type.kind==="NonNullType",L=K?k({},D,{type:D.type.type}):D;j=k({},L,{defaultValue:H})}else j=D;var ee=Object.entries(W).filter(function(C){var x=he(C,2);x[0];var T=x[1];return T<2}).map(function(C){var x=he(C,2),T=x[0];return x[1],T});if(j){var Y=n.props.setArgValue(j,!1);if(Y){var te=Y.definitions.find(function(C){return C.operation&&C.name&&C.name.value&&n.props.definition.name&&n.props.definition.name.value?C.name.value===n.props.definition.name.value:!1}),y=[].concat(R(te.variableDefinitions||[]),[j]).filter(function(C){return ee.indexOf(C.variable.name.value)===-1}),S=k({},te,{variableDefinitions:y}),w=Y.definitions,V=w.map(function(C){return te===C?S:C}),M=k({},Y,{definitions:V});n.props.onCommit(M)}}},m=function(){if(!(!s||!s.name||!s.name.value)){var b=s.name.value,v=(n.props.definition.variableDefinitions||[]).find(function(L){return L.variable.name.value===b});if(v){var E=v.defaultValue,_=n.props.setArgValue(E,{commit:!1});if(_){var A=_.definitions.find(function(L){return L.name.value===n.props.definition.name.value});if(!A)return;var D=0;(0,g.visit)(A,{Variable:function(ee){ee.name.value===b&&(D=D+1)}});var U=A.variableDefinitions||[];D<2&&(U=U.filter(function(L){return L.variable.name.value!==b}));var j=k({},A,{variableDefinitions:U}),W=_.definitions,H=W.map(function(L){return A===L?j:L}),K=k({},_,{definitions:H});n.props.onCommit(K)}}}},h=s&&s.kind==="Variable",F=this.state.displayArgActions?a.createElement("button",{type:"submit",className:"toolbar-button",title:h?"Remove the variable":"Extract the current value into a GraphQL variable",onClick:function(b){b.preventDefault(),b.stopPropagation(),h?m():p()},style:f.styles.actionButtonStyle},a.createElement("span",{style:{color:f.colors.variable}},"$")):null;return a.createElement("div",{style:{cursor:"pointer",minHeight:"16px",WebkitUserSelect:"none",userSelect:"none"},"data-arg-name":c.name,"data-arg-type":u.name,className:"graphiql-explorer-"+c.name},a.createElement("span",{style:{cursor:"pointer"},onClick:function(b){var v=!s;v?n.props.addArg(!0):n.props.removeArg(!0),n.setState({displayArgActions:v})}},(0,g.isInputObjectType)(u)?a.createElement("span",null,s?this.props.styleConfig.arrowOpen:this.props.styleConfig.arrowClosed):a.createElement(oe,{checked:!!s,styleConfig:this.props.styleConfig}),a.createElement("span",{style:{color:f.colors.attribute},title:c.description,onMouseEnter:function(){s!==null&&typeof s<"u"&&n.setState({displayArgActions:!0})},onMouseLeave:function(){return n.setState({displayArgActions:!1})}},c.name,ke(c)?"*":"",": ",F," ")," "),r||a.createElement("span",null)," ")}}]),t}(a.PureComponent),Je=function(i){B(t,i);function t(){var l,n,e,s;q(this,t);for(var c=arguments.length,f=Array(c),u=0;u0;E&&n.setState({displayFieldActions:!0})},onMouseLeave:function(){return n.setState({displayFieldActions:!1})}},(0,g.isObjectType)(o)?a.createElement("span",null,r?this.props.styleConfig.arrowOpen:this.props.styleConfig.arrowClosed):null,(0,g.isObjectType)(o)?null:a.createElement(oe,{checked:!!r,styleConfig:this.props.styleConfig}),a.createElement("span",{style:{color:u.colors.property},className:"graphiql-explorer-field-view"},s.name),this.state.displayFieldActions?a.createElement("button",{type:"submit",className:"toolbar-button",title:"Extract selections into a new reusable fragment",onClick:function(E){E.preventDefault(),E.stopPropagation();var _=o.name,A=_+"Fragment",D=(h||[]).filter(function(L){return L.name.value.startsWith(A)}).length;D>0&&(A=""+A+D);var U=r?r.selectionSet?r.selectionSet.selections:[]:[],j=[{kind:"FragmentSpread",name:{kind:"Name",value:A},directives:[]}],W={kind:"FragmentDefinition",name:{kind:"Name",value:A},typeCondition:{kind:"NamedType",name:{kind:"Name",value:o.name}},directives:[],selectionSet:{kind:"SelectionSet",selections:U}},H=n._modifyChildSelections(j,!1);if(H){var K=k({},H,{definitions:[].concat(R(H.definitions),[W])});n.props.onCommit(K)}else console.warn("Unable to complete extractFragment operation")},style:k({},u.styles.actionButtonStyle)},a.createElement("span",null,"…")):null),r&&p.length?a.createElement("div",{style:{marginLeft:16},className:"graphiql-explorer-graphql-arguments"},p.map(function(v){return a.createElement(Qe,{key:v.name,parentField:s,arg:v,selection:r,modifyArguments:n._setArguments,getDefaultScalarArgValue:n.props.getDefaultScalarArgValue,makeDefaultArg:n.props.makeDefaultArg,onRunOperation:n.props.onRunOperation,styleConfig:n.props.styleConfig,onCommit:n.props.onCommit,definition:n.props.definition})})):null);if(r&&((0,g.isObjectType)(o)||(0,g.isInterfaceType)(o)||(0,g.isUnionType)(o))){var d=(0,g.isUnionType)(o)?{}:o.getFields(),b=r?r.selectionSet?r.selectionSet.selections:[]:[];return a.createElement("div",{className:"graphiql-explorer-"+s.name},F,a.createElement("div",{style:{marginLeft:16}},h?h.map(function(v){var E=c.getType(v.typeCondition.name.value),_=v.name.value;return E?a.createElement(Ye,{key:_,fragment:v,selections:b,modifySelections:n._modifyChildSelections,schema:c,styleConfig:n.props.styleConfig,onCommit:n.props.onCommit}):null}):null,Object.keys(d).sort().map(function(v){return a.createElement(t,{key:v,field:d[v],selections:b,modifySelections:n._modifyChildSelections,schema:c,getDefaultFieldNames:f,getDefaultScalarArgValue:n.props.getDefaultScalarArgValue,makeDefaultArg:n.props.makeDefaultArg,onRunOperation:n.props.onRunOperation,styleConfig:n.props.styleConfig,onCommit:n.props.onCommit,definition:n.props.definition,availableFragments:n.props.availableFragments})}),(0,g.isInterfaceType)(o)||(0,g.isUnionType)(o)?c.getPossibleTypes(o).map(function(v){return a.createElement(Je,{key:v.name,implementingType:v,selections:b,modifySelections:n._modifyChildSelections,schema:c,getDefaultFieldNames:f,getDefaultScalarArgValue:n.props.getDefaultScalarArgValue,makeDefaultArg:n.props.makeDefaultArg,onRunOperation:n.props.onRunOperation,styleConfig:n.props.styleConfig,onCommit:n.props.onCommit,definition:n.props.definition})}):null))}return F}}]),t}(a.PureComponent);function Xe(i){try{return i.trim()?(0,g.parse)(i,{noLocation:!0}):null}catch(t){return new Error(t)}}var et={kind:"OperationDefinition",operation:"query",variableDefinitions:[],name:{kind:"Name",value:"MyQuery"},directives:[],selectionSet:{kind:"SelectionSet",selections:[]}},se={kind:"Document",definitions:[et]},J=null;function tt(i){if(J&&J[0]===i)return J[1];var t=Xe(i);return t?t instanceof Error?J?J[1]:se:(J=[i,t],t):se}var Oe={buttonStyle:{fontSize:"1.2em",padding:"0px",backgroundColor:"white",border:"none",margin:"5px 0px",height:"40px",width:"100%",display:"block",maxWidth:"none"},actionButtonStyle:{padding:"0px",backgroundColor:"white",border:"none",margin:"0px",maxWidth:"none",height:"15px",width:"15px",display:"inline-block",fontSize:"smaller"},explorerActionsStyle:{margin:"4px -8px -8px",paddingLeft:"8px",bottom:"0px",width:"100%",textAlign:"center",background:"none",borderTop:"none",borderBottom:"none"}},nt=function(i){B(t,i);function t(){var l,n,e,s;q(this,t);for(var c=arguments.length,f=Array(c),u=0;u"u"?"undefined":Pe(Z))==="object"&&typeof Z.commit<"u"?de=Z.commit:de=!0,G){var ge=k({},d,{definitions:d.definitions.map(function(Te){return Te===y?G:Te})});return de&&ne(ge),ge}else return d},schema:s,getDefaultFieldNames:b,getDefaultScalarArgValue:v,makeDefaultArg:f,onRunOperation:function(){n.props.onRunOperation&&n.props.onRunOperation(w)},styleConfig:u,availableFragments:Y})}),te),K)}}]),t}(a.PureComponent);Fe.defaultProps={getDefaultFieldNames:Se,getDefaultScalarArgValue:Ce};var it=function(i){B(t,i);function t(){var l,n,e,s;q(this,t);for(var c=arguments.length,f=Array(c),u=0;uN.createElement("svg",{width:5,height:8,viewBox:"0 0 5 8",fill:"currentColor",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...l},i?N.createElement("title",{id:t},i):null,N.createElement("path",{d:"M0.910453 6.86965L3.88955 3.89061C4.09782 3.68233 4.09782 3.34465 3.88955 3.13637L0.910453 0.157278C0.574475 -0.178701 0 0.0592511 0 0.534408V6.49259C0 6.96768 0.574475 7.20565 0.910453 6.86965Z"})),lt=({title:i,titleId:t,...l})=>N.createElement("svg",{height:"1em",strokeWidth:1.5,viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...l},i?N.createElement("title",{id:t},i):null,N.createElement("path",{d:"M18 6H20M22 6H20M20 6V4M20 6V8",strokeLinecap:"round",strokeLinejoin:"round"}),N.createElement("path",{d:"M21.4 20H2.6C2.26863 20 2 19.7314 2 19.4V11H21.4C21.7314 11 22 11.2686 22 11.6V19.4C22 19.7314 21.7314 20 21.4 20Z",strokeLinecap:"round",strokeLinejoin:"round"}),N.createElement("path",{d:"M2 11V4.6C2 4.26863 2.26863 4 2.6 4H8.77805C8.92127 4 9.05977 4.05124 9.16852 4.14445L12.3315 6.85555C12.4402 6.94876 12.5787 7 12.722 7H14",strokeLinecap:"round",strokeLinejoin:"round"})),st=({title:i,titleId:t,...l})=>N.createElement("svg",{width:15,height:15,viewBox:"0 0 15 15",xmlns:"http://www.w3.org/2000/svg",stroke:"currentColor",fill:"none","aria-labelledby":t,...l},i?N.createElement("title",{id:t},i):null,N.createElement("circle",{cx:7.5,cy:7.5,r:6,strokeWidth:2})),ut=({title:i,titleId:t,...l})=>N.createElement("svg",{width:15,height:15,viewBox:"0 0 15 15",xmlns:"http://www.w3.org/2000/svg",fill:"currentColor","aria-labelledby":t,...l},i?N.createElement("title",{id:t},i):null,N.createElement("circle",{cx:7.5,cy:7.5,r:7.5}),N.createElement("path",{d:"M4.64641 7.00106L6.8801 9.23256L10.5017 5.61325",stroke:"white",strokeWidth:1.5})),pt={keyword:"hsl(var(--color-primary))",def:"hsl(var(--color-tertiary))",property:"hsl(var(--color-info))",qualifier:"hsl(var(--color-secondary))",attribute:"hsl(var(--color-tertiary))",number:"hsl(var(--color-success))",string:"hsl(var(--color-warning))",builtin:"hsl(var(--color-success))",string2:"hsl(var(--color-secondary))",variable:"hsl(var(--color-secondary))",atom:"hsl(var(--color-tertiary))"},ct=P.createElement(De,{style:{width:"var(--px-16)",transform:"rotate(90deg)"}}),ft=P.createElement(De,{style:{width:"var(--px-16)"}}),mt=P.createElement(st,{style:{marginRight:"var(--px-4)"}}),dt=P.createElement(ut,{style:{fill:"hsl(var(--color-info))",marginRight:"var(--px-4)"}}),gt={buttonStyle:{cursor:"pointer",fontSize:"2em",lineHeight:0},explorerActionsStyle:{paddingTop:"var(--px-16)"},actionButtonStyle:{}};function vt(i){const{setOperationName:t}=X.useEditorContext({nonNull:!0}),{schema:l}=X.useSchemaContext({nonNull:!0}),{run:n}=X.useExecutionContext({nonNull:!0}),e=P.useCallback(f=>{f&&t(f),n()},[n,t]),[s,c]=X.useOptimisticState(X.useOperationsEditorState());return P.createElement(xe,{schema:l,onRunOperation:e,explorerIsOpen:!0,colors:pt,arrowOpen:ct,arrowClosed:ft,checkboxUnchecked:mt,checkboxChecked:dt,styles:gt,query:s,onEdit:c,...i})}function yt(i){return{title:"GraphiQL Explorer",icon:lt,content:()=>P.createElement(vt,{...i})}}z.explorerPlugin=yt,Object.defineProperty(z,Symbol.toStringTag,{value:"Module"})}); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react"),require("@graphiql/react"),require("graphql")):"function"==typeof define&&define.amd?define(["exports","react","@graphiql/react","graphql"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).GraphiQLPluginExplorer={},e.React,e.GraphiQL.React,e.GraphiQL.GraphQL)}(this,(function(e,t,n,r){"use strict";function i(e){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e)for(const n in e)if("default"!==n){const r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:()=>e[n]})}return t.default=e,Object.freeze(t)}const o=i(t),a=i(r);function l(e){return e&&Object.prototype.hasOwnProperty.call(e,"default")&&1===Object.keys(e).length?e.default:e}var s={},p={};const u=l(o),c=l(a);var f,d;function m(){if(f)return p;f=1,Object.defineProperty(p,"__esModule",{value:!0});var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(){return function(e,t){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return function(e,t){var n=[],r=!0,i=!1,o=void 0;try{for(var a,l=e[Symbol.iterator]();!(r=(a=l.next()).done)&&(n.push(a.value),!t||n.length!==t);r=!0);}catch(s){i=!0,o=s}finally{try{!r&&l.return&&l.return()}finally{if(i)throw o}}return n}(e,t);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),n=Object.assign||function(e){for(var t=1;t0?""+r+i:r;var u=s.type.toString(),c={kind:"VariableDefinition",variable:{kind:"Variable",name:{kind:"Name",value:p}},type:(0,o.parseType)(u),directives:[]},f=void 0,d={};if(null!=a){var m=(0,o.visit)(a,{Variable:function(t){var n,r=t.name.value,i=(n=r,(e.props.definition.variableDefinitions||[]).find((function(e){return e.variable.name.value===n})));if(d[r]=d[r]+1||1,i)return i.defaultValue}}),g="NonNullType"===c.type.kind?n({},c,{type:c.type.type}):c;f=n({},g,{defaultValue:m})}else f=c;var y=Object.entries(d).filter((function(e){var n=t(e,2);return n[0],n[1]<2})).map((function(e){var n=t(e,2),r=n[0];return n[1],r}));if(f){var v=e.props.setArgValue(f,!1);if(v){var h=v.definitions.find((function(t){return!!(t.operation&&t.name&&t.name.value&&e.props.definition.name&&e.props.definition.name.value)&&t.name.value===e.props.definition.name.value})),b=[].concat(l(h.variableDefinitions||[]),[f]).filter((function(e){return-1===y.indexOf(e.variable.name.value)})),k=n({},h,{variableDefinitions:b}),S=v.definitions.map((function(e){return h===e?k:e})),E=n({},v,{definitions:S});e.props.onCommit(E)}}},m=a&&"Variable"===a.kind,g=this.state.displayArgActions?i.createElement("button",{type:"submit",className:"toolbar-button",title:m?"Remove the variable":"Extract the current value into a GraphQL variable",onClick:function(t){t.preventDefault(),t.stopPropagation(),m?function(){if(a&&a.name&&a.name.value){var t=a.name.value,r=(e.props.definition.variableDefinitions||[]).find((function(e){return e.variable.name.value===t}));if(r){var i=r.defaultValue,l=e.props.setArgValue(i,{commit:!1});if(l){var s=l.definitions.find((function(t){return t.name.value===e.props.definition.name.value}));if(!s)return;var p=0;(0,o.visit)(s,{Variable:function(e){e.name.value===t&&(p+=1)}});var u=s.variableDefinitions||[];p<2&&(u=u.filter((function(e){return e.variable.name.value!==t})));var c=n({},s,{variableDefinitions:u}),f=l.definitions.map((function(e){return s===e?c:e})),d=n({},l,{definitions:f});e.props.onCommit(d)}}}}():d()},style:p.styles.actionButtonStyle},i.createElement("span",{style:{color:p.colors.variable}},"$")):null;return i.createElement("div",{style:{cursor:"pointer",minHeight:"16px",WebkitUserSelect:"none",userSelect:"none"},"data-arg-name":s.name,"data-arg-type":u.name,className:"graphiql-explorer-"+s.name},i.createElement("span",{style:{cursor:"pointer"},onClick:function(t){var n=!a;n?e.props.addArg(!0):e.props.removeArg(!0),e.setState({displayArgActions:n})}},(0,o.isInputObjectType)(u)?i.createElement("span",null,a?this.props.styleConfig.arrowOpen:this.props.styleConfig.arrowClosed):i.createElement(S,{checked:!!a,styleConfig:this.props.styleConfig}),i.createElement("span",{style:{color:p.colors.attribute},title:s.description,onMouseEnter:function(){null!=a&&e.setState({displayArgActions:!0})},onMouseLeave:function(){return e.setState({displayArgActions:!1})}},s.name,C(s)?"*":"",": ",g," ")," "),c||i.createElement("span",null)," ")}}]),a}(i.PureComponent),T=function(e){function t(){var e,n,r;s(this,t);for(var i=arguments.length,o=Array(i),a=0;a0&&e.setState({displayFieldActions:!0})},onMouseLeave:function(){return e.setState({displayFieldActions:!1})}},(0,o.isObjectType)(f)?i.createElement("span",null,c?this.props.styleConfig.arrowOpen:this.props.styleConfig.arrowClosed):null,(0,o.isObjectType)(f)?null:i.createElement(S,{checked:!!c,styleConfig:this.props.styleConfig}),i.createElement("span",{style:{color:u.colors.property},className:"graphiql-explorer-field-view"},a.name),this.state.displayFieldActions?i.createElement("button",{type:"submit",className:"toolbar-button",title:"Extract selections into a new reusable fragment",onClick:function(t){t.preventDefault(),t.stopPropagation();var r=f.name+"Fragment",i=(g||[]).filter((function(e){return e.name.value.startsWith(r)})).length;i>0&&(r=""+r+i);var o=c&&c.selectionSet?c.selectionSet.selections:[],a=[{kind:"FragmentSpread",name:{kind:"Name",value:r},directives:[]}],s={kind:"FragmentDefinition",name:{kind:"Name",value:r},typeCondition:{kind:"NamedType",name:{kind:"Name",value:f.name}},directives:[],selectionSet:{kind:"SelectionSet",selections:o}},p=e._modifyChildSelections(a,!1);if(p){var u=n({},p,{definitions:[].concat(l(p.definitions),[s])});e.props.onCommit(u)}else console.warn("Unable to complete extractFragment operation")},style:n({},u.styles.actionButtonStyle)},i.createElement("span",null,"…")):null),c&&d.length?i.createElement("div",{style:{marginLeft:16},className:"graphiql-explorer-graphql-arguments"},d.map((function(t){return i.createElement(F,{key:t.name,parentField:a,arg:t,selection:c,modifyArguments:e._setArguments,getDefaultScalarArgValue:e.props.getDefaultScalarArgValue,makeDefaultArg:e.props.makeDefaultArg,onRunOperation:e.props.onRunOperation,styleConfig:e.props.styleConfig,onCommit:e.props.onCommit,definition:e.props.definition})}))):null);if(c&&((0,o.isObjectType)(f)||(0,o.isInterfaceType)(f)||(0,o.isUnionType)(f))){var v=(0,o.isUnionType)(f)?{}:f.getFields(),h=c&&c.selectionSet?c.selectionSet.selections:[];return i.createElement("div",{className:"graphiql-explorer-"+a.name},y,i.createElement("div",{style:{marginLeft:16}},g?g.map((function(t){var n=s.getType(t.typeCondition.name.value),r=t.name.value;return n?i.createElement(N,{key:r,fragment:t,selections:h,modifySelections:e._modifyChildSelections,schema:s,styleConfig:e.props.styleConfig,onCommit:e.props.onCommit}):null})):null,Object.keys(v).sort().map((function(n){return i.createElement(t,{key:n,field:v[n],selections:h,modifySelections:e._modifyChildSelections,schema:s,getDefaultFieldNames:p,getDefaultScalarArgValue:e.props.getDefaultScalarArgValue,makeDefaultArg:e.props.makeDefaultArg,onRunOperation:e.props.onRunOperation,styleConfig:e.props.styleConfig,onCommit:e.props.onCommit,definition:e.props.definition,availableFragments:e.props.availableFragments})})),(0,o.isInterfaceType)(f)||(0,o.isUnionType)(f)?s.getPossibleTypes(f).map((function(t){return i.createElement(T,{key:t.name,implementingType:t,selections:h,modifySelections:e._modifyChildSelections,schema:s,getDefaultFieldNames:p,getDefaultScalarArgValue:e.props.getDefaultScalarArgValue,makeDefaultArg:e.props.makeDefaultArg,onRunOperation:e.props.onRunOperation,styleConfig:e.props.styleConfig,onCommit:e.props.onCommit,definition:e.props.definition})})):null))}return y}}]),t}(i.PureComponent);var P={kind:"Document",definitions:[{kind:"OperationDefinition",operation:"query",variableDefinitions:[],name:{kind:"Name",value:"MyQuery"},directives:[],selectionSet:{kind:"SelectionSet",selections:[]}}]},I=null;function R(e){if(I&&I[0]===e)return I[1];var t=function(e){try{return e.trim()?(0,o.parse)(e,{noLocation:!0}):null}catch(t){return new Error(t)}}(e);return t?t instanceof Error?I?I[1]:P:(I=[e,t],t):P}var q={buttonStyle:{fontSize:"1.2em",padding:"0px",backgroundColor:"white",border:"none",margin:"5px 0px",height:"40px",width:"100%",display:"block",maxWidth:"none"},actionButtonStyle:{padding:"0px",backgroundColor:"white",border:"none",margin:"0px",maxWidth:"none",height:"15px",width:"15px",display:"inline-block",fontSize:"smaller"},explorerActionsStyle:{margin:"4px -8px -8px",paddingLeft:"8px",bottom:"0px",width:"100%",textAlign:"center",background:"none",borderTop:"none",borderBottom:"none"}},B=function(e){function t(){var e,r,i;s(this,t);for(var o=arguments.length,a=Array(o),l=0;lo.createElement("svg",{width:5,height:8,viewBox:"0 0 5 8",fill:"currentColor",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?o.createElement("title",{id:t},e):null,o.createElement("path",{d:"M0.910453 6.86965L3.88955 3.89061C4.09782 3.68233 4.09782 3.34465 3.88955 3.13637L0.910453 0.157278C0.574475 -0.178701 0 0.0592511 0 0.534408V6.49259C0 6.96768 0.574475 7.20565 0.910453 6.86965Z"})),v=({title:e,titleId:t,...n})=>o.createElement("svg",{height:"1em",strokeWidth:1.5,viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?o.createElement("title",{id:t},e):null,o.createElement("path",{d:"M18 6H20M22 6H20M20 6V4M20 6V8",strokeLinecap:"round",strokeLinejoin:"round"}),o.createElement("path",{d:"M21.4 20H2.6C2.26863 20 2 19.7314 2 19.4V11H21.4C21.7314 11 22 11.2686 22 11.6V19.4C22 19.7314 21.7314 20 21.4 20Z"}),o.createElement("path",{d:"M2 11V4.6C2 4.26863 2.26863 4 2.6 4H8.77805C8.92127 4 9.05977 4.05124 9.16852 4.14445L12.3315 6.85555C12.4402 6.94876 12.5787 7 12.722 7H14",strokeLinecap:"round",strokeLinejoin:"round"})),h=({title:e,titleId:t,...n})=>o.createElement("svg",{width:15,height:15,viewBox:"0 0 15 15",xmlns:"http://www.w3.org/2000/svg",stroke:"currentColor",fill:"none","aria-labelledby":t,...n},e?o.createElement("title",{id:t},e):null,o.createElement("circle",{cx:7.5,cy:7.5,r:6,strokeWidth:2})),b=({title:e,titleId:t,...n})=>o.createElement("svg",{width:15,height:15,viewBox:"0 0 15 15",xmlns:"http://www.w3.org/2000/svg",fill:"currentColor","aria-labelledby":t,...n},e?o.createElement("title",{id:t},e):null,o.createElement("circle",{cx:7.5,cy:7.5,r:7.5}),o.createElement("path",{d:"M4.64641 7.00106L6.8801 9.23256L10.5017 5.61325",stroke:"white",strokeWidth:1.5})),k={keyword:"hsl(var(--color-primary))",def:"hsl(var(--color-tertiary))",property:"hsl(var(--color-info))",qualifier:"hsl(var(--color-secondary))",attribute:"hsl(var(--color-tertiary))",number:"hsl(var(--color-success))",string:"hsl(var(--color-warning))",builtin:"hsl(var(--color-success))",string2:"hsl(var(--color-secondary))",variable:"hsl(var(--color-secondary))",atom:"hsl(var(--color-tertiary))"},S=t.createElement(y,{style:{width:"var(--px-16)",transform:"rotate(90deg)"}}),E=t.createElement(y,{style:{width:"var(--px-16)"}}),C=t.createElement(h,{style:{marginRight:"var(--px-4)"}}),_=t.createElement(b,{style:{fill:"hsl(var(--color-info))",marginRight:"var(--px-4)"}}),A={buttonStyle:{cursor:"pointer",fontSize:"2em",lineHeight:0},explorerActionsStyle:{paddingTop:"var(--px-16)"},actionButtonStyle:{}},O=e=>{const{setOperationName:r}=n.useEditorContext({nonNull:!0}),{schema:i}=n.useSchemaStore(),{run:o}=n.useExecutionContext({nonNull:!0}),a=t.useCallback((e=>{e&&r(e),o()}),[o,r]),[l,s]=n.useOptimisticState(n.useOperationsEditorState());return t.createElement(g.Explorer,{schema:i,onRunOperation:a,explorerIsOpen:!0,colors:k,arrowOpen:S,arrowClosed:E,checkboxUnchecked:C,checkboxChecked:_,styles:A,query:l,onEdit:s,...e})};e.explorerPlugin=function(e){return{title:"GraphiQL Explorer",icon:v,content:()=>t.createElement(O,{...e})}},Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})})); diff --git a/netbox/project-static/dist/graphiql/plugin-explorer-style.css b/netbox/project-static/dist/graphiql/plugin-explorer-style.css index 69beae9ec..411933b70 100644 --- a/netbox/project-static/dist/graphiql/plugin-explorer-style.css +++ b/netbox/project-static/dist/graphiql/plugin-explorer-style.css @@ -1 +1 @@ -.docExplorerWrap{height:unset!important;min-width:unset!important;width:unset!important}.docExplorerWrap svg{display:unset}.doc-explorer-title{font-size:var(--font-size-h2);font-weight:var(--font-weight-medium)}.doc-explorer-rhs{display:none}.graphiql-explorer-root{font-family:var(--font-family-mono)!important;font-size:var(--font-size-body)!important;padding:0!important}.graphiql-explorer-root>div>div{border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important;padding-top:var(--px-16)}.graphiql-explorer-root input{background:unset}.graphiql-explorer-root select{background:hsl(var(--color-base))!important;border:1px solid hsla(var(--color-neutral),var(--alpha-secondary));border-radius:var(--border-radius-4);color:hsl(var(--color-neutral))!important;margin:0 var(--px-8);padding:var(--px-4) var(--px-6)}.graphiql-operation-title-bar .toolbar-button{line-height:0;margin-left:var(--px-8);color:hsla(var(--color-neutral),var(--alpha-secondary, .6));font-size:var(--font-size-h3);vertical-align:middle}.graphiql-explorer-graphql-arguments input{line-height:0}.graphiql-explorer-actions{border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important} +.docExplorerWrap{height:unset!important;min-width:unset!important;width:unset!important}.docExplorerWrap svg{display:unset}.doc-explorer-title{font-size:var(--font-size-h2);font-weight:var(--font-weight-medium)}.doc-explorer-rhs{display:none}.graphiql-explorer-root{font-family:var(--font-family-mono)!important;font-size:var(--font-size-body)!important;padding:0!important}.graphiql-explorer-root>div>div{padding-top:var(--px-16);border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important}.graphiql-explorer-root>div{overflow:auto!important}.graphiql-explorer-root input{background:unset}.graphiql-explorer-root select{border:1px solid hsla(var(--color-neutral),var(--alpha-secondary));border-radius:var(--border-radius-4);margin:0 var(--px-8);padding:var(--px-4)var(--px-6);background:hsl(var(--color-base))!important;color:hsl(var(--color-neutral))!important}.toolbar-button{all:unset;cursor:pointer;margin-left:var(--px-6);color:hsl(var(--color-primary));line-height:0!important;font-size:var(--font-size-h3)!important}.graphiql-explorer-slug .toolbar-button,.graphiql-explorer-graphql-arguments .toolbar-button{font-size:inherit!important}.graphiql-explorer-graphql-arguments input{min-width:2rem;line-height:0}.graphiql-explorer-actions{border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important} diff --git a/netbox/project-static/netbox-graphiql/package.json b/netbox/project-static/netbox-graphiql/package.json index 96291d5e4..6d39a68df 100644 --- a/netbox/project-static/netbox-graphiql/package.json +++ b/netbox/project-static/netbox-graphiql/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "dependencies": { - "@graphiql/plugin-explorer": "3.2.6", + "@graphiql/plugin-explorer": "4.0.6", "graphiql": "4.1.2", "graphql": "16.12.0", "js-cookie": "3.0.5", diff --git a/netbox/project-static/yarn.lock b/netbox/project-static/yarn.lock index 7deb5c7ea..dd06e4cb4 100644 --- a/netbox/project-static/yarn.lock +++ b/netbox/project-static/yarn.lock @@ -294,10 +294,10 @@ react-compiler-runtime "19.1.0-rc.1" zustand "^5" -"@graphiql/plugin-explorer@3.2.6": - version "3.2.6" - resolved "https://registry.npmjs.org/@graphiql/plugin-explorer/-/plugin-explorer-3.2.6.tgz" - integrity sha512-MXzG/zVNzZfes4Em253bHyAbD/lwwAZkPKvxCAQkjz0i3dtcv4uF3D8iqJ7214iu3SCphbORYZZUC93fik1yew== +"@graphiql/plugin-explorer@4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@graphiql/plugin-explorer/-/plugin-explorer-4.0.6.tgz#bec1207dc27334914590ab31f46c2e944bbf4ebf" + integrity sha512-TppIi92YPER3v70nlF01KTQrq9AiYqkZicSd1hpU7aqGmbqw/pLwBNLUEcfENBoJtw574Qxjswb01+GaYK0Tzw== dependencies: graphiql-explorer "^0.9.0" From edbfd0bae6b44ccd28a25bc731d17c4c153ea715 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 12 Jan 2026 16:40:42 -0500 Subject: [PATCH 04/10] Fixes #21117: Avoid exception when attempting to create v2 token without API_TOKEN_PEPPERS defined (#21132) --- netbox/users/models/tokens.py | 3 +++ netbox/users/tests/test_models.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/netbox/users/models/tokens.py b/netbox/users/models/tokens.py index bf51d6ef8..bc2fe197d 100644 --- a/netbox/users/models/tokens.py +++ b/netbox/users/models/tokens.py @@ -213,6 +213,9 @@ class Token(models.Model): def clean(self): super().clean() + if self.version == TokenVersionChoices.V2 and not settings.API_TOKEN_PEPPERS: + raise ValidationError(_("Unable to save v2 tokens: API_TOKEN_PEPPERS is not defined.")) + if self._state.adding: if self.pepper_id is not None and self.pepper_id not in settings.API_TOKEN_PEPPERS: raise ValidationError(_( diff --git a/netbox/users/tests/test_models.py b/netbox/users/tests/test_models.py index 367a82373..df3192260 100644 --- a/netbox/users/tests/test_models.py +++ b/netbox/users/tests/test_models.py @@ -1,9 +1,10 @@ from datetime import timedelta from django.core.exceptions import ValidationError -from django.test import TestCase +from django.test import TestCase, override_settings from django.utils import timezone +from users.choices import TokenVersionChoices from users.models import User, Token from utilities.testing import create_test_user @@ -94,6 +95,15 @@ class TokenTest(TestCase): token.refresh_from_db() self.assertEqual(token.description, 'New Description') + @override_settings(API_TOKEN_PEPPERS={}) + def test_v2_without_peppers_configured(self): + """ + Attempting to save a v2 token without API_TOKEN_PEPPERS defined should raise a ValidationError. + """ + token = Token(version=TokenVersionChoices.V2) + with self.assertRaises(ValidationError): + token.clean() + class UserConfigTest(TestCase): From c0f79df91f7393d39b15caf363acae034868178e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 12 Jan 2026 16:41:25 -0500 Subject: [PATCH 05/10] Introduce a new issue type for feature removals (#21092) Co-authored-by: Jason Novinger --- .github/ISSUE_TEMPLATE/06-deprecation.yaml | 16 ++++++++++----- .../ISSUE_TEMPLATE/07-feature_removal.yaml | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/07-feature_removal.yaml diff --git a/.github/ISSUE_TEMPLATE/06-deprecation.yaml b/.github/ISSUE_TEMPLATE/06-deprecation.yaml index 8a8e29b27..99c64ba24 100644 --- a/.github/ISSUE_TEMPLATE/06-deprecation.yaml +++ b/.github/ISSUE_TEMPLATE/06-deprecation.yaml @@ -1,20 +1,26 @@ --- -name: 🗑️ Deprecation +name: ⚠️ Deprecation type: Deprecation -description: The removal of an existing feature or resource +description: Designation of a feature or behavior that will be removed in a future release labels: ["netbox", "type: deprecation"] body: - type: textarea attributes: - label: Proposed Changes + label: Deprecated Functionality description: > - Describe in detail the proposed changes. What is being removed? + Describe the feature(s) and/or behavior that is being flagged for deprecation. + validations: + required: true + - type: input + attributes: + label: Scheduled removal + description: In what future release will the deprecated functionality be removed? validations: required: true - type: textarea attributes: label: Justification - description: Please provide justification for the proposed change(s). + description: Please provide justification for the deprecation. validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/07-feature_removal.yaml b/.github/ISSUE_TEMPLATE/07-feature_removal.yaml new file mode 100644 index 000000000..837bc2704 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/07-feature_removal.yaml @@ -0,0 +1,20 @@ +--- +name: 🗑️ Feature Removal +type: Removal +description: The removal of a deprecated feature or resource +labels: ["netbox", "type: removal"] +body: + - type: input + attributes: + label: Deprecation Issue + description: Specify the issue in which this deprecation was announced. + placeholder: "#1234" + validations: + required: true + - type: textarea + attributes: + label: Summary of Changes + description: > + List all changes necessary to remove the deprecated feature or resource. + validations: + required: true From ae03723e43b1d651a63ef6d8b75802ae7ce7d69d Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 13 Jan 2026 01:17:35 +0000 Subject: [PATCH 06/10] Fixes #21105: Update help text for token field on API page. (#21106) Co-authored-by: Jason Novinger --- netbox/users/forms/model_forms.py | 2 +- netbox/users/models/tokens.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index cc95b0ece..4c1a6a1eb 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -123,7 +123,7 @@ class UserTokenForm(forms.ModelForm): token = forms.CharField( label=_('Token'), help_text=_( - 'Tokens must be at least 40 characters in length. Be sure to record your key prior to ' + 'Tokens must be at least 40 characters in length. Be sure to record your token prior to ' 'submitting this form, as it will no longer be accessible once the token has been created.' ), widget=forms.TextInput( diff --git a/netbox/users/models/tokens.py b/netbox/users/models/tokens.py index bc2fe197d..e58fc5830 100644 --- a/netbox/users/models/tokens.py +++ b/netbox/users/models/tokens.py @@ -69,7 +69,7 @@ class Token(models.Model): write_enabled = models.BooleanField( verbose_name=_('write enabled'), default=True, - help_text=_('Permit create/update/delete operations using this key') + help_text=_('Permit create/update/delete operations using this token') ) # For legacy v1 tokens, this field stores the plaintext 40-char token value. Not used for v2. plaintext = models.CharField( From e14934e5a54fbf980f144700425cf093c2d4624d Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 05:05:43 +0000 Subject: [PATCH 07/10] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 67adfc129..6f6be3d55 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-08 05:04+0000\n" +"POT-Creation-Date: 2026-01-13 05:05+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1822,7 +1822,6 @@ msgid "ASN Count" msgstr "" #: netbox/circuits/tables/virtual_circuits.py:64 -#: netbox/netbox/navigation/menu.py:235 #: netbox/templates/circuits/virtualcircuit.html:87 #: netbox/templates/vpn/l2vpn.html:60 netbox/templates/vpn/tunnel.html:72 #: netbox/vpn/tables/tunnels.py:59 @@ -12190,6 +12189,10 @@ msgstr "" msgid "L2VPNs" msgstr "" +#: netbox/netbox/navigation/menu.py:235 +msgid "L2VPN Terminations" +msgstr "" + #: netbox/netbox/navigation/menu.py:241 msgid "IKE Proposals" msgstr "" @@ -15938,7 +15941,7 @@ msgstr "" #: netbox/users/forms/model_forms.py:126 msgid "" "Tokens must be at least 40 characters in length. Be sure to record " -"your key prior to submitting this form, as it will no longer be " +"your token prior to submitting this form, as it will no longer be " "accessible once the token has been created." msgstr "" @@ -16077,7 +16080,7 @@ msgid "write enabled" msgstr "" #: netbox/users/models/tokens.py:72 -msgid "Permit create/update/delete operations using this key" +msgid "Permit create/update/delete operations using this token" msgstr "" #: netbox/users/models/tokens.py:76 @@ -16126,12 +16129,16 @@ msgstr "" msgid "tokens" msgstr "" -#: netbox/users/models/tokens.py:219 +#: netbox/users/models/tokens.py:217 +msgid "Unable to save v2 tokens: API_TOKEN_PEPPERS is not defined." +msgstr "" + +#: netbox/users/models/tokens.py:222 #, python-brace-format msgid "Invalid pepper ID: {id}. Check configured API_TOKEN_PEPPERS." msgstr "" -#: netbox/users/models/tokens.py:232 +#: netbox/users/models/tokens.py:235 #, python-brace-format msgid "" "Expiration time must be in the future. Current server time is {current_time} " From e60807adc5d7b5dce69468527aa038f3979fa582 Mon Sep 17 00:00:00 2001 From: Mark Robert Coleman Date: Tue, 13 Jan 2026 16:58:06 +0100 Subject: [PATCH 08/10] Fixes #21121: Expand changelog message doc/add cross-references (#21138) --- docs/features/change-logging.md | 6 ++++-- docs/integrations/rest-api.md | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/features/change-logging.md b/docs/features/change-logging.md index 73e23709c..48cf8756d 100644 --- a/docs/features/change-logging.md +++ b/docs/features/change-logging.md @@ -10,9 +10,11 @@ Change records are exposed in the API via the read-only endpoint `/api/extras/ob ## User Messages -!!! info "This feature was introduced in NetBox v4.4." +When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message (up to 200 characters) that will appear in the change record. This can be helpful to capture additional context, such as the reason for a change or a reference to an external ticket. -When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message that will appear in the change record. This can be helpful to capture additional context, such as the reason for the change. +When editing an object via the web UI, the "Changelog message" field appears at the bottom of the form. This field is optional. The changelog message field is available in object create forms, object edit forms, delete confirmation dialogs, and bulk operations. + +For information on including changelog messages when making changes via the REST API, see [Changelog Messages](../integrations/rest-api.md#changelog-messages). ## Correlating Changes by Request diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md index 2b97f601c..66a95d924 100644 --- a/docs/integrations/rest-api.md +++ b/docs/integrations/rest-api.md @@ -610,9 +610,7 @@ http://netbox/api/dcim/sites/ \ ## Changelog Messages -!!! info "This feature was introduced in NetBox v4.4." - -Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Beginning in NetBox v4.4, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation. +Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Additionally, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation. For example, the following API request will create a new site and record a message in the resulting changelog entry: @@ -628,7 +626,7 @@ http://netbox/api/dcim/sites/ \ }' ``` -This approach works when creating, modifying, or deleting objects, either individually or in bulk. +This approach works when creating, modifying, or deleting objects, either individually or in bulk. For more information about change logging, see [Change Logging](../features/change-logging.md). ## Uploading Files From 6bd083b7ed4c5a6767a3be23bf26832b3eb26e02 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 14 Jan 2026 09:06:55 -0500 Subject: [PATCH 09/10] Closes #21142: Enable filtering device components by site/location/rack directly via GraphQL API (#21145) --- netbox/dcim/graphql/filter_mixins.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/netbox/dcim/graphql/filter_mixins.py b/netbox/dcim/graphql/filter_mixins.py index c02c89948..50ce98cfb 100644 --- a/netbox/dcim/graphql/filter_mixins.py +++ b/netbox/dcim/graphql/filter_mixins.py @@ -38,6 +38,15 @@ class ScopedFilterMixin: @dataclass class ComponentModelFilterMixin: + _site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field(name='site') + ) + _location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field(name='location') + ) + _rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field(name='rack') + ) device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() device_id: ID | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field() From 434334d927c4488a5edb1d23e101a1a6f7633fbb Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Wed, 14 Jan 2026 14:50:35 -0600 Subject: [PATCH 10/10] Fixes #20239: Prevent shared mutable state in PluginMenuItem and PluginMenuButton (#21099) PluginMenuItem and PluginMenuButton classes used mutable class-level defaults for `permissions` and `buttons` attributes, causing permission leakage between instances when these attributes were modified without explicit parameters. Changed to initialize these attributes as fresh lists per instance in __init__ when not explicitly provided, following standard Python pattern for avoiding mutable default arguments. --- netbox/netbox/plugins/navigation.py | 9 ++++-- netbox/netbox/tests/test_plugins.py | 45 ++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/netbox/netbox/plugins/navigation.py b/netbox/netbox/plugins/navigation.py index 2b18a4a0e..2062e95d5 100644 --- a/netbox/netbox/plugins/navigation.py +++ b/netbox/netbox/plugins/navigation.py @@ -37,8 +37,6 @@ class PluginMenuItem: Alternatively, a pre-generated url can be set on the object which will be rendered literally. Buttons are each specified as a list of PluginMenuButton instances. """ - permissions = [] - buttons = [] _url = None def __init__( @@ -54,10 +52,14 @@ class PluginMenuItem: if type(permissions) not in (list, tuple): raise TypeError(_("Permissions must be passed as a tuple or list.")) self.permissions = permissions + else: + self.permissions = [] if buttons is not None: if type(buttons) not in (list, tuple): raise TypeError(_("Buttons must be passed as a tuple or list.")) self.buttons = buttons + else: + self.buttons = [] @property def url(self): @@ -74,7 +76,6 @@ class PluginMenuButton: ButtonColorChoices. """ color = ButtonColorChoices.DEFAULT - permissions = [] _url = None def __init__(self, link, title, icon_class, color=None, permissions=None): @@ -87,6 +88,8 @@ class PluginMenuButton: if type(permissions) not in (list, tuple): raise TypeError(_("Permissions must be passed as a tuple or list.")) self.permissions = permissions + else: + self.permissions = [] if color is not None: if color not in ButtonColorChoices.values(): raise ValueError(_("Button color must be a choice within ButtonColorChoices.")) diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py index 550dca514..a8595d10d 100644 --- a/netbox/netbox/tests/test_plugins.py +++ b/netbox/netbox/tests/test_plugins.py @@ -11,7 +11,7 @@ from netbox.tests.dummy_plugin import config as dummy_config from netbox.tests.dummy_plugin.data_backends import DummyBackend from netbox.tests.dummy_plugin.jobs import DummySystemJob from netbox.tests.dummy_plugin.webhook_callbacks import set_context -from netbox.plugins.navigation import PluginMenu +from netbox.plugins.navigation import PluginMenu, PluginMenuItem, PluginMenuButton from netbox.plugins.utils import get_plugin_config from netbox.graphql.schema import Query from netbox.registry import registry @@ -227,3 +227,46 @@ class PluginTest(TestCase): Test the registration of webhook callbacks. """ self.assertIn(set_context, registry['webhook_callbacks']) + + +class PluginNavigationTest(TestCase): + + def test_plugin_menu_item_independent_permissions(self): + item1 = PluginMenuItem(link='test1', link_text='Test 1') + item1.permissions.append('leaked_permission') + + item2 = PluginMenuItem(link='test2', link_text='Test 2') + + self.assertIsNot(item1.permissions, item2.permissions) + self.assertEqual(item1.permissions, ['leaked_permission']) + self.assertEqual(item2.permissions, []) + + def test_plugin_menu_item_independent_buttons(self): + item1 = PluginMenuItem(link='test1', link_text='Test 1') + button = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test') + item1.buttons.append(button) + + item2 = PluginMenuItem(link='test2', link_text='Test 2') + + self.assertIsNot(item1.buttons, item2.buttons) + self.assertEqual(len(item1.buttons), 1) + self.assertEqual(item1.buttons[0], button) + self.assertEqual(item2.buttons, []) + + def test_plugin_menu_button_independent_permissions(self): + button1 = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test') + button1.permissions.append('leaked_permission') + + button2 = PluginMenuButton(link='button2', title='Button 2', icon_class='mdi-test') + + self.assertIsNot(button1.permissions, button2.permissions) + self.assertEqual(button1.permissions, ['leaked_permission']) + self.assertEqual(button2.permissions, []) + + def test_explicit_permissions_remain_independent(self): + item1 = PluginMenuItem(link='test1', link_text='Test 1', permissions=['explicit_permission']) + item2 = PluginMenuItem(link='test2', link_text='Test 2', permissions=['different_permission']) + + self.assertIsNot(item1.permissions, item2.permissions) + self.assertEqual(item1.permissions, ['explicit_permission']) + self.assertEqual(item2.permissions, ['different_permission'])