Skip to content

Commit

Permalink
Merge pull request #7218 from netbox-community/7162-base-path-bug2
Browse files Browse the repository at this point in the history
Fixes #7162: Decouple base path rendering from API request logic
  • Loading branch information
jeremystretch committed Sep 8, 2021
2 parents 851f8a1 + cf8fdac commit 8c1a01d
Show file tree
Hide file tree
Showing 18 changed files with 61 additions and 128 deletions.
4 changes: 2 additions & 2 deletions netbox/project-static/dist/config.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions netbox/project-static/dist/config.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions netbox/project-static/dist/jobs.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions netbox/project-static/dist/jobs.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions netbox/project-static/dist/lldp.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions netbox/project-static/dist/lldp.js.map

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions netbox/project-static/dist/netbox.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions netbox/project-static/dist/netbox.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions netbox/project-static/dist/status.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions netbox/project-static/dist/status.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions netbox/project-static/src/buttons/connectionToggle.ts
Expand Up @@ -8,12 +8,12 @@ import { isTruthy, apiPatch, hasError, getElements } from '../util';
* @param element Connection Toggle Button Element
*/
function toggleConnection(element: HTMLButtonElement): void {
const id = element.getAttribute('data');
const url = element.getAttribute('data-url');
const connected = element.classList.contains('connected');
const status = connected ? 'planned' : 'connected';

if (isTruthy(id)) {
apiPatch(`/api/dcim/cables/${id}/`, { status }).then(res => {
if (isTruthy(url)) {
apiPatch(url, { status }).then(res => {
if (hasError(res)) {
// If the API responds with an error, show it to the user.
createToast('danger', 'Error', res.error).show();
Expand Down
23 changes: 12 additions & 11 deletions netbox/project-static/src/jobs.ts
Expand Up @@ -4,7 +4,7 @@ import { apiGetBase, hasError, getNetboxData } from './util';
let timeout: number = 1000;

interface JobInfo {
id: Nullable<string>;
url: Nullable<string>;
complete: boolean;
}

Expand All @@ -23,15 +23,16 @@ function asyncTimeout(ms: number) {
function getJobInfo(): JobInfo {
let complete = false;

const id = getNetboxData('data-job-id');
const jobComplete = getNetboxData('data-job-complete');
// Determine the API URL for the job status
const url = getNetboxData('data-job-url');

// Determine the job completion status, if present. If the job is not complete, the value will be
// "None". Otherwise, it will be a stringified date.
const jobComplete = getNetboxData('data-job-complete');
if (typeof jobComplete === 'string' && jobComplete.toLowerCase() !== 'none') {
complete = true;
}
return { id, complete };
return { url, complete };
}

/**
Expand Down Expand Up @@ -59,10 +60,10 @@ function updateLabel(status: JobStatus) {

/**
* Recursively check the job's status.
* @param id Job ID
* @param url API URL for job result
*/
async function checkJobStatus(id: string) {
const res = await apiGetBase<APIJobResult>(`/api/extras/job-results/${id}/`);
async function checkJobStatus(url: string) {
const res = await apiGetBase<APIJobResult>(url);
if (hasError(res)) {
// If the response is an API error, display an error message and stop checking for job status.
const toast = createToast('danger', 'Error', res.error);
Expand All @@ -82,17 +83,17 @@ async function checkJobStatus(id: string) {
if (timeout < 10000) {
timeout += 1000;
}
await Promise.all([checkJobStatus(id), asyncTimeout(timeout)]);
await Promise.all([checkJobStatus(url), asyncTimeout(timeout)]);
}
}
}

function initJobs() {
const { id, complete } = getJobInfo();
const { url, complete } = getJobInfo();

if (id !== null && !complete) {
if (url !== null && !complete) {
// If there is a job ID and it is not completed, check for the job's status.
Promise.resolve(checkJobStatus(id));
Promise.resolve(checkJobStatus(url));
}
}

Expand Down
18 changes: 15 additions & 3 deletions netbox/project-static/src/tableConfig.ts
Expand Up @@ -53,8 +53,8 @@ function removeColumns(event: Event): void {
/**
* Submit form configuration to the NetBox API.
*/
async function submitFormConfig(formConfig: Dict<Dict>): Promise<APIResponse<APIUserConfig>> {
return await apiPatch<APIUserConfig>('/api/users/config/', formConfig);
async function submitFormConfig(url: string, formConfig: Dict<Dict>): Promise<APIResponse<APIUserConfig>> {
return await apiPatch<APIUserConfig>(url, formConfig);
}

/**
Expand All @@ -66,6 +66,18 @@ function handleSubmit(event: Event): void {

const element = event.currentTarget as HTMLFormElement;

// Get the API URL for submitting the form
const url = element.getAttribute('data-url');
if (url == null) {
const toast = createToast(
'danger',
'Error Updating Table Configuration',
'No API path defined for configuration form.'
);
toast.show();
return;
}

// Get all the selected options from any select element in the form.
const options = getSelectedOptions(element);

Expand All @@ -83,7 +95,7 @@ function handleSubmit(event: Event): void {
const data = path.reduceRight<Dict<Dict>>((value, key) => ({ [key]: value }), formData);

// Submit the resulting object to the API to update the user's preferences for this table.
submitFormConfig(data).then(res => {
submitFormConfig(url, data).then(res => {
if (hasError(res)) {
const toast = createToast('danger', 'Error Updating Table Configuration', res.error);
toast.show();
Expand Down
82 changes: 1 addition & 81 deletions netbox/project-static/src/util.ts
@@ -1,7 +1,4 @@
import Cookie from 'cookie';
import queryString from 'query-string';

import type { Stringifiable } from 'query-string';

type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
type ReqData = URLSearchParams | Dict | undefined | unknown;
Expand Down Expand Up @@ -107,84 +104,8 @@ function getCsrfToken(): string {
return csrfToken;
}

/**
* Get the NetBox `settings.BASE_PATH` from the `<html/>` element's data attributes.
*
* @returns If there is no `BASE_PATH` specified, the return value will be `''`.
*/ function getBasePath(): string {
const value = document.documentElement.getAttribute('data-netbox-base-path');
if (value === null) {
return '';
}
return value;
}

/**
* Constrict an object from a URL query param string, with all values as an array.
*
* @param params URL search query string.
* @returns Constructed query object.
*/
function queryParamsToObject(params: string): Record<string, Stringifiable[]> {
const result = {} as Record<string, Stringifiable[]>;
const searchParams = new URLSearchParams(params);
for (const [key, originalValue] of searchParams.entries()) {
let value = [] as Stringifiable[];
if (key in result) {
value = [...value, ...result[key]];
}
if (Array.isArray(originalValue)) {
value = [...value, ...originalValue];
} else {
value = [...value, originalValue];
}
result[key] = value;
}
return result;
}

/**
* Build a NetBox URL that includes `settings.BASE_PATH` and enforces leading and trailing slashes.
*
* @example
* ```js
* // With a BASE_PATH of 'netbox/'
* const url = buildUrl('/api/dcim/devices');
* console.log(url);
* // => /netbox/api/dcim/devices/
* ```
*
* @param path Relative path _after_ (excluding) the `BASE_PATH`.
*/
function buildUrl(destination: string): string {
// Separate the path from any URL search params.
const [pathname, search] = destination.split(/(?=\?)/g);

// If the `origin` exists in the API path (as in the case of paginated responses), remove it.
const origin = new RegExp(window.location.origin, 'g');
const path = pathname.replaceAll(origin, '');

const basePath = getBasePath();

// Combine `BASE_PATH` with this request's path, removing _all_ slashes.
let combined = [...basePath.split('/'), ...path.split('/')].filter(p => p);

if (combined[0] !== '/') {
// Ensure the URL has a leading slash.
combined = ['', ...combined];
}
if (combined[combined.length - 1] !== '/') {
// Ensure the URL has a trailing slash.
combined = [...combined, ''];
}
const url = combined.join('/');
// Construct an object from the URL search params so it can be re-serialized with the new URL.
const query = queryParamsToObject(search);
return queryString.stringifyUrl({ url, query });
}

export async function apiRequest<R extends Dict, D extends ReqData = undefined>(
path: string,
url: string,
method: Method,
data?: D,
): Promise<APIResponse<R>> {
Expand All @@ -196,7 +117,6 @@ export async function apiRequest<R extends Dict, D extends ReqData = undefined>(
body = JSON.stringify(data);
headers.set('content-type', 'application/json');
}
const url = buildUrl(path);

const res = await fetch(url, { method, body, headers, credentials: 'same-origin' });
const contentType = res.headers.get('Content-Type');
Expand Down
4 changes: 2 additions & 2 deletions netbox/templates/dcim/inc/cable_toggle_buttons.html
@@ -1,10 +1,10 @@
{% if perms.dcim.change_cable %}
{% if cable.status == 'connected' %}
<button type="button" class="btn btn-warning btn-sm cable-toggle connected" title="Mark Planned" data="{{ cable.pk }}">
<button type="button" class="btn btn-warning btn-sm cable-toggle connected" title="Mark Planned" data-url="{% url 'dcim-api:cable-detail' pk=cable.pk %}">
<i class="mdi mdi-lan-disconnect" aria-hidden="true"></i>
</button>
{% else %}
<button type="button" class="btn btn-info btn-sm cable-toggle" title="Mark Installed" data="{{ cable.pk }}">
<button type="button" class="btn btn-info btn-sm cable-toggle" title="Mark Installed" data-url="{% url 'dcim-api:cable-detail' pk=cable.pk %}">
<i class="mdi mdi-lan-connect" aria-hidden="true"></i>
</button>
{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/extras/report_result.html
Expand Up @@ -96,6 +96,6 @@ <h5 class="card-header">
{% endblock %}

{% block data %}
<span data-job-id="{{ result.pk }}"></span>
<span data-job-url="{% url 'extras-api:jobresult-detail' pk=result.pk %}"></span>
<span data-job-complete="{{ result.completed }}"></span>
{% endblock %}
2 changes: 1 addition & 1 deletion netbox/templates/extras/script_result.html
Expand Up @@ -112,6 +112,6 @@ <h5 class="card-header">
{% endblock content-wrapper %}

{% block data %}
<span data-job-id="{{ result.pk }}"></span>
<span data-job-url="{% url 'extras-api:jobresult-detail' pk=result.pk %}"></span>
<span data-job-complete="{{ result.completed }}"></span>
{% endblock %}
Expand Up @@ -7,7 +7,7 @@
<h5 class="modal-title">Table Configuration</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form class="form-horizontal userconfigform" data-config-root="tables.{{ form.table_name }}">
<form class="form-horizontal userconfigform" data-url="{% url 'users-api:userconfig-list' %}" data-config-root="tables.{{ form.table_name }}">
<div class="modal-body row">
<div class="col-5 text-center">
{{ form.available_columns.label }}
Expand Down

0 comments on commit 8c1a01d

Please sign in to comment.