mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[React] API Image Functionality (#5696)
* 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 * Allow CORS request to /media/ * Add ApiImage component - Used to fetch images from API which require auth - Requires some tweaks to back-end CORS settings - Otherwrise, image loading won't work on new API * Update build order table * Remove debug code * Update part detail page
This commit is contained in:
parent
65e9ba0633
commit
4de51f4f4f
@ -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(
|
||||
|
@ -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) => {
|
||||
|
60
src/frontend/src/components/images/ApiImage.tsx
Normal file
60
src/frontend/src/components/images/ApiImage.tsx
Normal file
@ -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<string>('');
|
||||
|
||||
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 (
|
||||
<Stack>
|
||||
<LoadingOverlay visible={imgQuery.isLoading || imgQuery.isFetching} />
|
||||
<Image {...props} src={image} />
|
||||
{imgQuery.isError && <Overlay color="#F00" />}
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<Image
|
||||
src={url}
|
||||
<ApiImage
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={size}
|
||||
fit="contain"
|
||||
@ -49,20 +47,21 @@ export function ThumbnailHoverCard({
|
||||
alt?: string;
|
||||
size?: number;
|
||||
}) {
|
||||
function MainGroup() {
|
||||
const card = useMemo(() => {
|
||||
return (
|
||||
<Group position="left" spacing={10}>
|
||||
<Group position="left" spacing={10} noWrap={true}>
|
||||
<Thumbnail src={src} alt={alt} size={size} />
|
||||
<Text>{text}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
}, [src, text, alt, size]);
|
||||
|
||||
if (link)
|
||||
return (
|
||||
<Anchor href={link} style={{ textDecoration: 'none' }}>
|
||||
<MainGroup />
|
||||
{card}
|
||||
</Anchor>
|
||||
);
|
||||
return <MainGroup />;
|
||||
|
||||
return <div>{card}</div>;
|
||||
}
|
@ -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({
|
||||
<Stack spacing="xs">
|
||||
<Group position="apart">
|
||||
<Group position="left">
|
||||
<StylishText size="xl">{title}</StylishText>
|
||||
{subtitle && <Text size="lg">{subtitle}</Text>}
|
||||
<Stack spacing="xs">
|
||||
{title && <StylishText size="xl">{title}</StylishText>}
|
||||
{subtitle && <Text size="lg">{subtitle}</Text>}
|
||||
{detail}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Space />
|
||||
{actions && <Group position="right">{actions}</Group>}
|
||||
</Group>
|
||||
{detail}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 && (
|
||||
<Text>{part.full_name}</Text>
|
||||
// <ThumbnailHoverCard
|
||||
// src={part.thumbnail || part.image}
|
||||
// text={part.full_name}
|
||||
// link=""
|
||||
// />
|
||||
<ThumbnailHoverCard
|
||||
src={part.thumbnail || part.image}
|
||||
text={part.full_name}
|
||||
alt={part.description}
|
||||
link=""
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
<Text>{record.full_name}</Text>
|
||||
<Group spacing="xs" align="left" noWrap={true}>
|
||||
<Thumbnail
|
||||
src={record.thumbnail || record.image}
|
||||
alt={record.name}
|
||||
size={24}
|
||||
/>
|
||||
<Text>{record.full_name}</Text>
|
||||
</Group>
|
||||
// <ThumbnailHoverCard
|
||||
// src={record.thumbnail || record.image}
|
||||
// text={record.name}
|
||||
|
@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms';
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||
import { Thumbnail } from '../../items/Thumbnail';
|
||||
import { Thumbnail } from '../../images/Thumbnail';
|
||||
import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
@ -35,6 +35,8 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
||||
let part = getPart(record);
|
||||
return (
|
||||
<Group
|
||||
noWrap={true}
|
||||
position="left"
|
||||
onClick={() => {
|
||||
navigate(`/part/${part.pk}/`);
|
||||
}}
|
||||
|
@ -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<any>({});
|
||||
|
||||
@ -54,8 +58,8 @@ export function useInstance({
|
||||
return null;
|
||||
});
|
||||
},
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false
|
||||
refetchOnMount: refetchOnMount,
|
||||
refetchOnWindowFocus: refetchOnWindowFocus
|
||||
});
|
||||
|
||||
const refreshInstance = useCallback(function () {
|
||||
|
@ -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 (
|
||||
<Group spacing="xs" noWrap={true}>
|
||||
<ApiImage
|
||||
src={String(part.image || '')}
|
||||
radius="sm"
|
||||
height={64}
|
||||
width={64}
|
||||
/>
|
||||
<Stack spacing="xs">
|
||||
<Text size="lg" weight={500}>
|
||||
{part.full_name}
|
||||
</Text>
|
||||
<Text size="sm">{part.description}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}, [part, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={t`Part`}
|
||||
subtitle={part.full_name}
|
||||
detail={
|
||||
<Alert color="teal" title="Part detail goes here">
|
||||
<Text>TODO: Part details</Text>
|
||||
</Alert>
|
||||
}
|
||||
detail={partDetail}
|
||||
breadcrumbs={breadcrumbs}
|
||||
actions={[
|
||||
<Button
|
||||
|
Loading…
Reference in New Issue
Block a user