From 814322e512c348cb62fa1ea853078ddfa1cb41fb Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 Oct 2023 23:25:17 +1100 Subject: [PATCH] Api tweaks (#5690) * Improvements to API handling on react UI - Do not force "/api/" prefix to the base URL of the server - We will need to fetch media files from the server (at /media/) - Extend API URL helper functions * Update some more hard-coded URLs * Fix search API endpoint * Fix div for panel tab * Fix debug msg --- .../src/components/forms/InstanceOptions.tsx | 4 +-- .../src/components/modals/QrCodeModal.tsx | 21 +++++++------ .../src/components/nav/BreadcrumbList.tsx | 5 ++- src/frontend/src/components/nav/Header.tsx | 3 +- .../src/components/nav/NavHoverMenu.tsx | 2 +- .../src/components/nav/NotificationDrawer.tsx | 3 +- .../src/components/nav/PanelGroup.tsx | 1 + .../src/components/nav/SearchDrawer.tsx | 3 +- .../components/renderers/GeneralRenderer.tsx | 4 +-- .../tables/build/BuildOrderTable.tsx | 4 +-- .../notifications/NotificationsTable.tsx | 4 +-- .../tables/part/PartCategoryTable.tsx | 4 +-- .../src/components/tables/part/PartTable.tsx | 4 +-- .../tables/part/RelatedPartTable.tsx | 4 +-- .../tables/stock/StockItemTable.tsx | 5 ++- .../tables/stock/StockLocationTable.tsx | 4 +-- src/frontend/src/functions/auth.tsx | 20 ++++++++---- src/frontend/src/functions/forms.tsx | 4 +-- src/frontend/src/hooks/UseInstance.tsx | 8 +++-- src/frontend/src/main.tsx | 4 +-- src/frontend/src/pages/Auth/Login.tsx | 4 +-- src/frontend/src/pages/Auth/Set-Password.tsx | 4 +-- .../src/pages/Index/Profile/UserPanel.tsx | 6 ++-- src/frontend/src/pages/Index/Scan.tsx | 4 +-- src/frontend/src/pages/build/BuildDetail.tsx | 4 +-- src/frontend/src/pages/part/PartDetail.tsx | 4 +-- src/frontend/src/pages/stock/StockDetail.tsx | 4 +-- src/frontend/src/states/ApiState.tsx | 31 ++++++++++++++++--- src/frontend/src/states/SettingsState.tsx | 6 ++-- src/frontend/src/states/UserState.tsx | 6 ++-- 30 files changed, 114 insertions(+), 70 deletions(-) diff --git a/src/frontend/src/components/forms/InstanceOptions.tsx b/src/frontend/src/components/forms/InstanceOptions.tsx index 5fad7cd007..9dc1c1115f 100644 --- a/src/frontend/src/components/forms/InstanceOptions.tsx +++ b/src/frontend/src/components/forms/InstanceOptions.tsx @@ -26,7 +26,7 @@ export function InstanceOptions({ ]); const hostListData = Object.keys(hostList).map((key) => ({ value: key, - label: hostList[key].name + label: hostList[key]?.name })); function SaveOptions(newHostList: HostList): void { @@ -93,7 +93,7 @@ function ServerInfo({ return ( - {hostList[hostKey].host} + {hostList[hostKey]?.host}
Version: {server.version}
diff --git a/src/frontend/src/components/modals/QrCodeModal.tsx b/src/frontend/src/components/modals/QrCodeModal.tsx index afc9eac3cc..a8fb969023 100644 --- a/src/frontend/src/components/modals/QrCodeModal.tsx +++ b/src/frontend/src/components/modals/QrCodeModal.tsx @@ -23,6 +23,7 @@ import { Html5QrcodeResult } from 'html5-qrcode/core'; import { useEffect, useState } from 'react'; import { api } from '../../App'; +import { ApiPaths, apiUrl } from '../../states/ApiState'; export function QrCodeModal({ context, @@ -63,16 +64,18 @@ export function QrCodeModal({ qrCodeScanner?.pause(); handlers.append(decodedText); - api.post('/barcode/', { barcode: decodedText }).then((response) => { - showNotification({ - title: response.data?.success || t`Unknown response`, - message: JSON.stringify(response.data), - color: response.data?.success ? 'teal' : 'red' + api + .post(apiUrl(ApiPaths.barcode), { barcode: decodedText }) + .then((response) => { + showNotification({ + title: response.data?.success || t`Unknown response`, + message: JSON.stringify(response.data), + color: response.data?.success ? 'teal' : 'red' + }); + if (response.data?.url) { + window.location.href = response.data.url; + } }); - if (response.data?.url) { - window.location.href = response.data.url; - } - }); qrCodeScanner?.resume(); } diff --git a/src/frontend/src/components/nav/BreadcrumbList.tsx b/src/frontend/src/components/nav/BreadcrumbList.tsx index 0619933a04..4b59c6a00a 100644 --- a/src/frontend/src/components/nav/BreadcrumbList.tsx +++ b/src/frontend/src/components/nav/BreadcrumbList.tsx @@ -17,7 +17,10 @@ export function BreadcrumbList({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) { {breadcrumbs.map((breadcrumb, index) => { return ( - breadcrumb.url && navigate(breadcrumb.url)}> + breadcrumb.url && navigate(breadcrumb.url)} + > {breadcrumb.name} ); diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index 18ae20dbf2..d9160df35e 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -8,6 +8,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { api } from '../../App'; import { navTabs as mainNavTabs } from '../../defaults/links'; import { InvenTreeStyle } from '../../globalStyle'; +import { ApiPaths, apiUrl } from '../../states/ApiState'; import { ScanButton } from '../items/ScanButton'; import { MainMenu } from './MainMenu'; import { NavHoverMenu } from './NavHoverMenu'; @@ -36,7 +37,7 @@ export function Header() { queryKey: ['notification-count'], queryFn: async () => { return api - .get('/notifications/', { + .get(apiUrl(ApiPaths.notifications_list), { params: { read: false, limit: 1 diff --git a/src/frontend/src/components/nav/NavHoverMenu.tsx b/src/frontend/src/components/nav/NavHoverMenu.tsx index 29d8ac0234..3a05e1815a 100644 --- a/src/frontend/src/components/nav/NavHoverMenu.tsx +++ b/src/frontend/src/components/nav/NavHoverMenu.tsx @@ -34,7 +34,7 @@ export function NavHoverMenu({ useEffect(() => { if (hostKey && hostList[hostKey]) { - setInstanceName(hostList[hostKey].name); + setInstanceName(hostList[hostKey]?.name); } }, [hostKey]); diff --git a/src/frontend/src/components/nav/NotificationDrawer.tsx b/src/frontend/src/components/nav/NotificationDrawer.tsx index acd5e3b1df..ace710f608 100644 --- a/src/frontend/src/components/nav/NotificationDrawer.tsx +++ b/src/frontend/src/components/nav/NotificationDrawer.tsx @@ -15,6 +15,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { api } from '../../App'; +import { ApiPaths, apiUrl } from '../../states/ApiState'; /** * Construct a notification drawer. @@ -33,7 +34,7 @@ export function NotificationDrawer({ queryKey: ['notifications', opened], queryFn: async () => api - .get('/notifications/', { + .get(apiUrl(ApiPaths.notifications_list), { params: { read: false, limit: 10 diff --git a/src/frontend/src/components/nav/PanelGroup.tsx b/src/frontend/src/components/nav/PanelGroup.tsx index eca3b61ea9..fb9332a8d4 100644 --- a/src/frontend/src/components/nav/PanelGroup.tsx +++ b/src/frontend/src/components/nav/PanelGroup.tsx @@ -68,6 +68,7 @@ export function PanelGroup({ (panel, idx) => !panel.hidden && ( { return api - .get(url(api_key, pk)) + .get(apiUrl(api_key, pk)) .then((res) => res.data) .catch(() => { { diff --git a/src/frontend/src/components/tables/build/BuildOrderTable.tsx b/src/frontend/src/components/tables/build/BuildOrderTable.tsx index e874763b22..ce426dc790 100644 --- a/src/frontend/src/components/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/components/tables/build/BuildOrderTable.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTableRefresh } from '../../../hooks/TableRefresh'; -import { ApiPaths, url } from '../../../states/ApiState'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; @@ -142,7 +142,7 @@ export function BuildOrderTable({ params = {} }: { params?: any }) { return ( { // Get token from server const token = await axios - .get(`${host}${url(ApiPaths.user_token)}`, { auth: { username, password } }) + .get(apiUrl(ApiPaths.user_token), { + auth: { username, password }, + baseURL: host.toString() + }) .then((response) => response.data.token) .catch((error) => { + showNotification({ + title: t`Login failed`, + message: t`Error fetching token from server.`, + color: 'red' + }); return false; }); @@ -50,7 +58,7 @@ export const doClassicLogout = async () => { export const doSimpleLogin = async (email: string) => { const { host } = useLocalState.getState(); const mail = await axios - .post(`${host}${url(ApiPaths.user_simple_login)}`, { + .post(apiUrl(ApiPaths.user_simple_login), { email: email }) .then((response) => response.data) @@ -77,7 +85,7 @@ export const doTokenLogin = (token: string) => { export function handleReset(navigate: any, values: { email: string }) { api - .post(url(ApiPaths.user_reset), values, { + .post(apiUrl(ApiPaths.user_reset), values, { headers: { Authorization: '' } }) .then((val) => { @@ -101,7 +109,7 @@ export function handleReset(navigate: any, values: { email: string }) { export function checkLoginState(navigate: any) { api - .get(url(ApiPaths.user_token)) + .get(apiUrl(ApiPaths.user_token)) .then((val) => { if (val.status === 200 && val.data.token) { doTokenLogin(val.data.token); diff --git a/src/frontend/src/functions/forms.tsx b/src/frontend/src/functions/forms.tsx index 93759ed4c8..be32260935 100644 --- a/src/frontend/src/functions/forms.tsx +++ b/src/frontend/src/functions/forms.tsx @@ -6,7 +6,7 @@ import { AxiosResponse } from 'axios'; import { api } from '../App'; import { ApiForm, ApiFormProps } from '../components/forms/ApiForm'; import { ApiFormFieldType } from '../components/forms/fields/ApiFormField'; -import { url } from '../states/ApiState'; +import { apiUrl } from '../states/ApiState'; import { invalidResponse, permissionDenied } from './notifications'; import { generateUniqueId } from './uid'; @@ -14,7 +14,7 @@ import { generateUniqueId } from './uid'; * Construct an API url from the provided ApiFormProps object */ export function constructFormUrl(props: ApiFormProps): string { - return url(props.url, props.pk); + return apiUrl(props.url, props.pk); } /** diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx index 31e1023003..110d1bf2a5 100644 --- a/src/frontend/src/hooks/UseInstance.tsx +++ b/src/frontend/src/hooks/UseInstance.tsx @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { useCallback, useState } from 'react'; import { api } from '../App'; -import { ApiPaths, url } from '../states/ApiState'; +import { ApiPaths, apiUrl } from '../states/ApiState'; /** * Custom hook for loading a single instance of an instance from the API @@ -32,8 +32,10 @@ export function useInstance({ return null; } + let url = apiUrl(endpoint, pk); + return api - .get(url(endpoint, pk), { + .get(url, { params: params }) .then((response) => { @@ -48,7 +50,7 @@ export function useInstance({ }) .catch((error) => { setInstance({}); - console.error(`Error fetching instance ${url}${pk}:`, error); + console.error(`Error fetching instance ${url}:`, error); return null; }); }, diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx index fc3fe6e53a..48afa14def 100644 --- a/src/frontend/src/main.tsx +++ b/src/frontend/src/main.tsx @@ -28,13 +28,13 @@ export const IS_DEV_OR_DEMO = IS_DEV || IS_DEMO; window.INVENTREE_SETTINGS = { server_list: { 'mantine-cqj63coxn': { - host: `${window.location.origin}/api/`, + host: `${window.location.origin}/`, name: 'Current Server' }, ...(IS_DEV_OR_DEMO ? { 'mantine-u56l5jt85': { - host: 'https://demo.inventree.org/api/', + host: 'https://demo.inventree.org/', name: 'InvenTree Demo' } } diff --git a/src/frontend/src/pages/Auth/Login.tsx b/src/frontend/src/pages/Auth/Login.tsx index 0452669083..f63b8ebbd1 100644 --- a/src/frontend/src/pages/Auth/Login.tsx +++ b/src/frontend/src/pages/Auth/Login.tsx @@ -22,12 +22,12 @@ export default function Login() { state.fetchServerApiState ]); const hostname = - hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey].name; + hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey]?.name; const [hostEdit, setHostEdit] = useToggle([false, true] as const); // Data manipulation functions function ChangeHost(newHost: string): void { - setHost(hostList[newHost].host, newHost); + setHost(hostList[newHost]?.host, newHost); setApiDefaults(); fetchServerApiState(); } diff --git a/src/frontend/src/pages/Auth/Set-Password.tsx b/src/frontend/src/pages/Auth/Set-Password.tsx index e288cd8b08..47e604650b 100644 --- a/src/frontend/src/pages/Auth/Set-Password.tsx +++ b/src/frontend/src/pages/Auth/Set-Password.tsx @@ -14,7 +14,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'; import { api } from '../../App'; import { LanguageContext } from '../../contexts/LanguageContext'; -import { ApiPaths, url } from '../../states/ApiState'; +import { ApiPaths, apiUrl } from '../../states/ApiState'; export default function Set_Password() { const simpleForm = useForm({ initialValues: { password: '' } }); @@ -57,7 +57,7 @@ export default function Set_Password() { // Set password with call to backend api .post( - url(ApiPaths.user_reset_set), + apiUrl(ApiPaths.user_reset_set), { uid: uid, token: token, diff --git a/src/frontend/src/pages/Index/Profile/UserPanel.tsx b/src/frontend/src/pages/Index/Profile/UserPanel.tsx index 5995a4a5ce..982391979d 100644 --- a/src/frontend/src/pages/Index/Profile/UserPanel.tsx +++ b/src/frontend/src/pages/Index/Profile/UserPanel.tsx @@ -19,6 +19,7 @@ import { api, queryClient } from '../../../App'; import { ColorToggle } from '../../../components/items/ColorToggle'; import { EditButton } from '../../../components/items/EditButton'; import { LanguageSelect } from '../../../components/items/LanguageSelect'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { useLocalState } from '../../../states/LocalState'; import { UserTheme } from './UserTheme'; @@ -29,7 +30,8 @@ export function UserPanel() { // data function fetchData() { - return api.get('user/me/').then((res) => res.data); + // TODO: Replace this call with the global user state, perhaps? + return api.get(apiUrl(ApiPaths.user_me)).then((res) => res.data); } const { isLoading, data } = useQuery({ queryKey: ['user-me'], @@ -68,7 +70,7 @@ export function UserInfo({ data }: { data: any }) { const form = useForm({ initialValues: data }); const [editing, setEditing] = useToggle([false, true] as const); function SaveData(values: any) { - api.put('user/me/', values).then((res) => { + api.put(apiUrl(ApiPaths.user_me)).then((res) => { if (res.status === 200) { setEditing(); queryClient.invalidateQueries(['user-me']); diff --git a/src/frontend/src/pages/Index/Scan.tsx b/src/frontend/src/pages/Index/Scan.tsx index 5e6f0f7f8e..6967bb23bd 100644 --- a/src/frontend/src/pages/Index/Scan.tsx +++ b/src/frontend/src/pages/Index/Scan.tsx @@ -50,7 +50,7 @@ import { TitleWithDoc } from '../../components/items/TitleWithDoc'; import { Render, RenderTypes } from '../../components/renderers'; import { notYetImplemented } from '../../functions/notifications'; import { IS_DEV_OR_DEMO } from '../../main'; -import { ApiPaths, url } from '../../states/ApiState'; +import { ApiPaths, apiUrl } from '../../states/ApiState'; interface ScanItem { id: string; @@ -136,7 +136,7 @@ export default function Scan() { function runBarcode(value: string, id?: string) { api - .post(url(ApiPaths.barcode), { barcode: value }) + .post(apiUrl(ApiPaths.barcode), { barcode: value }) .then((response) => { // update item in history if (!id) return; diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index e60237119f..95bb9261a7 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -24,7 +24,7 @@ import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { useInstance } from '../../hooks/UseInstance'; -import { ApiPaths, url } from '../../states/ApiState'; +import { ApiPaths, apiUrl } from '../../states/ApiState'; /** * Detail page for a single Build Order @@ -121,7 +121,7 @@ export default function BuildDetail() { icon: , content: ( diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 79b3834501..8df9f2df84 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -29,7 +29,7 @@ import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { editPart } from '../../functions/forms/PartForms'; import { useInstance } from '../../hooks/UseInstance'; -import { ApiPaths, url } from '../../states/ApiState'; +import { ApiPaths, apiUrl } from '../../states/ApiState'; /** * Detail view for a single Part instance @@ -157,7 +157,7 @@ export default function PartDetail() { // TODO: Set edit permission based on user permissions return ( diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index c096c757e2..dad5383e8e 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -18,7 +18,7 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { AttachmentTable } from '../../components/tables/AttachmentTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { useInstance } from '../../hooks/UseInstance'; -import { ApiPaths, url } from '../../states/ApiState'; +import { ApiPaths, apiUrl } from '../../states/ApiState'; export default function StockDetail() { const { id } = useParams(); @@ -87,7 +87,7 @@ export default function StockDetail() { icon: , content: ( diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index b4e3122824..024357be2d 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -15,13 +15,17 @@ export const useServerApiState = create((set, get) => ({ setServer: (newServer: ServerAPIProps) => set({ server: newServer }), fetchServerApiState: async () => { // Fetch server data - await api.get('/').then((response) => { + await api.get(apiUrl(ApiPaths.api_server_info)).then((response) => { set({ server: response.data }); }); } })); export enum ApiPaths { + api_server_info = 'api-server-info', + + api_search = 'api-search', + // User information user_me = 'api-user-me', user_roles = 'api-user-roles', @@ -62,11 +66,21 @@ export enum ApiPaths { sales_order_list = 'api-sales-order-list' } +/** + * Function to return the API prefix. + * For now it is fixed, but may be configurable in the future. + */ +export function apiPrefix(): string { + return '/api/'; +} + /** * Return the endpoint associated with a given API path */ -export function endpoint(path: ApiPaths): string { +export function apiEndpoint(path: ApiPaths): string { switch (path) { + case ApiPaths.api_server_info: + return ''; case ApiPaths.user_me: return 'user/me/'; case ApiPaths.user_roles: @@ -76,9 +90,13 @@ export function endpoint(path: ApiPaths): string { case ApiPaths.user_simple_login: return 'email/generate/'; case ApiPaths.user_reset: + // Note leading prefix here return '/auth/password/reset/'; case ApiPaths.user_reset_set: + // Note leading prefix here return '/auth/password/reset/confirm/'; + case ApiPaths.api_search: + return 'search/'; case ApiPaths.settings_global_list: return 'settings/global/'; case ApiPaths.settings_user_list: @@ -122,8 +140,13 @@ export function endpoint(path: ApiPaths): string { /** * Construct an API URL with an endpoint and (optional) pk value */ -export function url(path: ApiPaths, pk?: any): string { - let _url = endpoint(path); +export function apiUrl(path: ApiPaths, pk?: any): string { + let _url = apiEndpoint(path); + + // If the URL does not start with a '/', add the API prefix + if (!_url.startsWith('/')) { + _url = apiPrefix() + _url; + } if (_url && pk) { _url += `${pk}/`; diff --git a/src/frontend/src/states/SettingsState.tsx b/src/frontend/src/states/SettingsState.tsx index 6fc4985b58..1f0c46b2f3 100644 --- a/src/frontend/src/states/SettingsState.tsx +++ b/src/frontend/src/states/SettingsState.tsx @@ -4,7 +4,7 @@ import { create } from 'zustand'; import { api } from '../App'; -import { ApiPaths, url } from './ApiState'; +import { ApiPaths, apiUrl } from './ApiState'; import { Setting } from './states'; interface SettingsStateProps { @@ -20,7 +20,7 @@ export const useGlobalSettingsState = create( settings: [], fetchSettings: async () => { await api - .get(url(ApiPaths.settings_global_list)) + .get(apiUrl(ApiPaths.settings_global_list)) .then((response) => { set({ settings: response.data }); }) @@ -38,7 +38,7 @@ export const useUserSettingsState = create((set, get) => ({ settings: [], fetchSettings: async () => { await api - .get(url(ApiPaths.settings_user_list)) + .get(apiUrl(ApiPaths.settings_user_list)) .then((response) => { set({ settings: response.data }); }) diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx index 197d876cbb..a61034899c 100644 --- a/src/frontend/src/states/UserState.tsx +++ b/src/frontend/src/states/UserState.tsx @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { api } from '../App'; -import { ApiPaths, url } from './ApiState'; +import { ApiPaths, apiUrl } from './ApiState'; import { UserProps } from './states'; interface UserStateProps { @@ -19,7 +19,7 @@ export const useUserState = create((set, get) => ({ fetchUserState: async () => { // Fetch user data await api - .get(url(ApiPaths.user_me)) + .get(apiUrl(ApiPaths.user_me)) .then((response) => { const user: UserProps = { name: `${response.data.first_name} ${response.data.last_name}`, @@ -34,7 +34,7 @@ export const useUserState = create((set, get) => ({ // Fetch role data await api - .get(url(ApiPaths.user_roles)) + .get(apiUrl(ApiPaths.user_roles)) .then((response) => { const user: UserProps = get().user as UserProps;