diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 7179ffd737..0b7dba4cc9 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -139,8 +139,8 @@ ALLOWED_HOSTS = get_setting( # Cross Origin Resource Sharing (CORS) options -# Only allow CORS access to API -CORS_URLS_REGEX = r'^/api/.*$' +# Only allow CORS access to API and media endpoints +CORS_URLS_REGEX = r'^/(api|media)/.*$' # Extract CORS options from configuration file CORS_ORIGIN_ALLOW_ALL = get_boolean_setting( diff --git a/src/frontend/src/components/forms/fields/RelatedModelField.tsx b/src/frontend/src/components/forms/fields/RelatedModelField.tsx index c13cac5d9b..2bb346cd67 100644 --- a/src/frontend/src/components/forms/fields/RelatedModelField.tsx +++ b/src/frontend/src/components/forms/fields/RelatedModelField.tsx @@ -65,11 +65,6 @@ export function RelatedModelField({ if (formPk != null) { let url = (definition.api_url || '') + formPk + '/'; - // TODO: Fix this!! - if (url.startsWith('/api')) { - url = url.substring(4); - } - api.get(url).then((response) => { let data = response.data; @@ -105,13 +100,6 @@ export function RelatedModelField({ return null; } - // TODO: Fix this in the api controller - let url = definition.api_url; - - if (url.startsWith('/api')) { - url = url.substring(4); - } - let filters = definition.filters ?? {}; if (definition.adjustFilters) { @@ -126,7 +114,7 @@ export function RelatedModelField({ }; return api - .get(url, { + .get(definition.api_url, { params: params }) .then((response) => { diff --git a/src/frontend/src/components/images/ApiImage.tsx b/src/frontend/src/components/images/ApiImage.tsx new file mode 100644 index 0000000000..c82c3f6c62 --- /dev/null +++ b/src/frontend/src/components/images/ApiImage.tsx @@ -0,0 +1,60 @@ +/** + * Component for loading an image from the InvenTree server, + * using the API's token authentication. + * + * Image caching is handled automagically by the browsers cache + */ +import { + Image, + ImageProps, + LoadingOverlay, + Overlay, + Stack +} from '@mantine/core'; +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; + +import { api } from '../../App'; + +/** + * Construct an image container which will load and display the image + */ +export function ApiImage(props: ImageProps) { + const [image, setImage] = useState(''); + + const imgQuery = useQuery({ + queryKey: ['image', props.src], + enabled: props.src != undefined && props.src != null && props.src != '', + queryFn: async () => { + if (!props.src) { + return null; + } + return api + .get(props.src, { + responseType: 'blob' + }) + .then((response) => { + let img = new Blob([response.data], { + type: response.headers['content-type'] + }); + let url = URL.createObjectURL(img); + setImage(url); + return response; + }) + .catch((error) => { + console.error(`Error fetching image ${props.src}:`, error); + return null; + }); + }, + refetchOnMount: true, + refetchOnWindowFocus: true + }); + + return ( + + + + {imgQuery.isError && } + + ); +} diff --git a/src/frontend/src/components/items/Thumbnail.tsx b/src/frontend/src/components/images/Thumbnail.tsx similarity index 77% rename from src/frontend/src/components/items/Thumbnail.tsx rename to src/frontend/src/components/images/Thumbnail.tsx index cbef00ecb7..9ac12fca5b 100644 --- a/src/frontend/src/components/items/Thumbnail.tsx +++ b/src/frontend/src/components/images/Thumbnail.tsx @@ -2,8 +2,9 @@ import { t } from '@lingui/macro'; import { Anchor, Image } from '@mantine/core'; import { Group } from '@mantine/core'; import { Text } from '@mantine/core'; +import { useMemo } from 'react'; -import { api } from '../../App'; +import { ApiImage } from './ApiImage'; export function Thumbnail({ src, @@ -16,12 +17,9 @@ export function Thumbnail({ }) { // TODO: Use HoverCard to display a larger version of the image - // TODO: This is a hack until we work out the /api/ path issue - let url = api.getUri({ url: '..' + src }); - return ( - {alt} { return ( - + {text} ); - } + }, [src, text, alt, size]); if (link) return ( - + {card} ); - return ; + + return
{card}
; } diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx index c82aa62540..be9257f618 100644 --- a/src/frontend/src/components/nav/PageDetail.tsx +++ b/src/frontend/src/components/nav/PageDetail.tsx @@ -17,7 +17,7 @@ export function PageDetail({ breadcrumbs, actions }: { - title: string; + title?: string; subtitle?: string; detail?: ReactNode; breadcrumbs?: Breadcrumb[]; @@ -34,13 +34,15 @@ export function PageDetail({ - {title} - {subtitle && {subtitle}} + + {title && {title}} + {subtitle && {subtitle}} + {detail} + {actions && {actions}} - {detail} diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 0b8b0416f3..e310c48f57 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -3,7 +3,7 @@ import { Alert } from '@mantine/core'; import { Group, Text } from '@mantine/core'; import { ReactNode } from 'react'; -import { Thumbnail } from '../items/Thumbnail'; +import { Thumbnail } from '../images/Thumbnail'; import { RenderBuildOrder } from './Build'; import { RenderAddress, diff --git a/src/frontend/src/components/renderers/GeneralRenderer.tsx b/src/frontend/src/components/renderers/GeneralRenderer.tsx index 19275ff6b2..d100b14635 100644 --- a/src/frontend/src/components/renderers/GeneralRenderer.tsx +++ b/src/frontend/src/components/renderers/GeneralRenderer.tsx @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../../App'; import { ApiPaths, apiUrl } from '../../states/ApiState'; -import { ThumbnailHoverCard } from '../items/Thumbnail'; +import { ThumbnailHoverCard } from '../images/Thumbnail'; export function GeneralRenderer({ api_key, diff --git a/src/frontend/src/components/tables/build/BuildOrderTable.tsx b/src/frontend/src/components/tables/build/BuildOrderTable.tsx index ce426dc790..d45d69540d 100644 --- a/src/frontend/src/components/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/components/tables/build/BuildOrderTable.tsx @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { ThumbnailHoverCard } from '../../images/Thumbnail'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; @@ -28,12 +29,12 @@ function buildOrderTableColumns(): TableColumn[] { let part = record.part_detail; return ( part && ( - {part.full_name} - // + ) ); } diff --git a/src/frontend/src/components/tables/part/PartTable.tsx b/src/frontend/src/components/tables/part/PartTable.tsx index dd4bf2402e..49bdb5d58d 100644 --- a/src/frontend/src/components/tables/part/PartTable.tsx +++ b/src/frontend/src/components/tables/part/PartTable.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Text } from '@mantine/core'; +import { Group, Text } from '@mantine/core'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -8,6 +8,7 @@ import { notYetImplemented } from '../../../functions/notifications'; import { shortenString } from '../../../functions/tables'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { Thumbnail } from '../../images/Thumbnail'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable'; @@ -26,7 +27,14 @@ function partTableColumns(): TableColumn[] { render: function (record: any) { // TODO - Link to the part detail page return ( - {record.full_name} + + + {record.full_name} + // { navigate(`/part/${part.pk}/`); }} diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx index 110d1bf2a5..377cd7da27 100644 --- a/src/frontend/src/hooks/UseInstance.tsx +++ b/src/frontend/src/hooks/UseInstance.tsx @@ -16,11 +16,15 @@ import { ApiPaths, apiUrl } from '../states/ApiState'; export function useInstance({ endpoint, pk, - params = {} + params = {}, + refetchOnMount = false, + refetchOnWindowFocus = false }: { endpoint: ApiPaths; pk: string | undefined; params?: any; + refetchOnMount?: boolean; + refetchOnWindowFocus?: boolean; }) { const [instance, setInstance] = useState({}); @@ -54,8 +58,8 @@ export function useInstance({ return null; }); }, - refetchOnMount: false, - refetchOnWindowFocus: false + refetchOnMount: refetchOnMount, + refetchOnWindowFocus: refetchOnWindowFocus }); const refreshInstance = useCallback(function () { diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 8bf5892430..e903c95085 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1,5 +1,12 @@ import { t } from '@lingui/macro'; -import { Alert, Button, LoadingOverlay, Stack, Text } from '@mantine/core'; +import { + Alert, + Button, + Group, + LoadingOverlay, + Stack, + Text +} from '@mantine/core'; import { IconBuilding, IconCurrencyDollar, @@ -18,8 +25,11 @@ import { } from '@tabler/icons-react'; import React from 'react'; import { useMemo } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; +import { api } from '../../App'; +import { ApiImage } from '../../components/images/ApiImage'; +import { Thumbnail } from '../../components/images/Thumbnail'; import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; @@ -46,7 +56,9 @@ export default function PartDetail() { pk: id, params: { path_detail: true - } + }, + refetchOnMount: true, + refetchOnWindowFocus: true }); // Part data panels (recalculate when part data changes) @@ -185,18 +197,31 @@ export default function PartDetail() { [part] ); + const partDetail = useMemo(() => { + return ( + + + + + {part.full_name} + + {part.description} + + + ); + }, [part, id]); + return ( <> - TODO: Part details - - } + detail={partDetail} breadcrumbs={breadcrumbs} actions={[