feat: добавить preview image pipeline в admin
- добавлен polling variants и ручной refresh выбранного asset\n- добавлен picture/srcset preview с выбором preset\n- добавлен URL-state и copy actions для рабочих ссылок
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAssetPicture } from "./hooks/use-asset-picture.hook"
|
||||||
import { useAssetOverview } from "./hooks/use-asset-overview.hook"
|
import { useAssetOverview } from "./hooks/use-asset-overview.hook"
|
||||||
import { useAssetsDashboard } from "./hooks/use-assets-dashboard.hook"
|
import { useAssetsDashboard } from "./hooks/use-assets-dashboard.hook"
|
||||||
import { useCreateAsset } from "./hooks/use-create-asset.hook"
|
import { useCreateAsset } from "./hooks/use-create-asset.hook"
|
||||||
@@ -11,6 +12,7 @@ import type { AssetsFactory } from "./types/assets-factory.type"
|
|||||||
export const assetsFactory: AssetsFactory = () => {
|
export const assetsFactory: AssetsFactory = () => {
|
||||||
return {
|
return {
|
||||||
useAssetOverview,
|
useAssetOverview,
|
||||||
|
useAssetPicture,
|
||||||
useAssetsDashboard,
|
useAssetsDashboard,
|
||||||
useCreateAsset,
|
useCreateAsset,
|
||||||
useCreateAssetVersion,
|
useCreateAssetVersion,
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ export const ASSETS_DASHBOARD_LIST_PARAMS = {
|
|||||||
limit: "20",
|
limit: "20",
|
||||||
offset: "0",
|
offset: "0",
|
||||||
} satisfies ListAssetsParams
|
} satisfies ListAssetsParams
|
||||||
|
|
||||||
|
export const ASSET_PICTURE_SIZES = "(min-width: 992px) 34vw, 100vw"
|
||||||
|
|
||||||
|
export const ASSET_VARIANTS_POLLING_MS = 2000
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { useGetAsset, useGetAssetVariants } from "infra/backend-api"
|
import { useGetAsset, useGetAssetVariants, type AssetVariantsResponseDto } from "infra/backend-api"
|
||||||
|
|
||||||
|
import { ASSET_VARIANTS_POLLING_MS } from "../config/assets.config"
|
||||||
import type { AssetOverview } from "../types/assets-api.type"
|
import type { AssetOverview } from "../types/assets-api.type"
|
||||||
|
|
||||||
|
const isRunningVariantStatus = (status: string) => status === "pending" || status === "processing"
|
||||||
|
|
||||||
|
const hasRunningVariants = (variants: AssetVariantsResponseDto["variants"] = []) =>
|
||||||
|
variants.some((variant) => isRunningVariantStatus(variant.status))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные выбранного asset и его variants.
|
* Данные выбранного asset и его variants.
|
||||||
*/
|
*/
|
||||||
@@ -10,12 +16,23 @@ export const useAssetOverview = (publicId: string | null): AssetOverview => {
|
|||||||
const variantsQuery = useGetAssetVariants(
|
const variantsQuery = useGetAssetVariants(
|
||||||
publicId,
|
publicId,
|
||||||
assetQuery.data?.currentVersion ? String(assetQuery.data.currentVersion) : undefined,
|
assetQuery.data?.currentVersion ? String(assetQuery.data.currentVersion) : undefined,
|
||||||
|
{
|
||||||
|
refreshInterval: (data) => (hasRunningVariants(data?.variants) ? ASSET_VARIANTS_POLLING_MS : 0),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
const variants = variantsQuery.data?.variants ?? []
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
await Promise.all([assetQuery.mutate(), variantsQuery.mutate()])
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset: assetQuery.data ?? null,
|
asset: assetQuery.data ?? null,
|
||||||
error: assetQuery.error ?? variantsQuery.error,
|
error: assetQuery.error ?? variantsQuery.error,
|
||||||
|
hasRunningVariants: hasRunningVariants(variants),
|
||||||
isLoading: assetQuery.isLoading || variantsQuery.isLoading,
|
isLoading: assetQuery.isLoading || variantsQuery.isLoading,
|
||||||
variants: variantsQuery.data?.variants ?? [],
|
isRefreshing: assetQuery.isValidating || variantsQuery.isValidating,
|
||||||
|
refresh,
|
||||||
|
variants,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useGetAssetPicture } from "infra/backend-api"
|
||||||
|
|
||||||
|
import { ASSET_PICTURE_SIZES } from "../config/assets.config"
|
||||||
|
import type { AssetPicturePreview } from "../types/assets-api.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picture/srcset preview contract выбранного asset.
|
||||||
|
*/
|
||||||
|
export const useAssetPicture = (publicId: string | null, preset: string | null): AssetPicturePreview => {
|
||||||
|
const pictureQuery = preset ? { preset, sizes: ASSET_PICTURE_SIZES } : null
|
||||||
|
const picture = useGetAssetPicture(publicId, pictureQuery)
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
await picture.mutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: picture.error,
|
||||||
|
isLoading: picture.isLoading,
|
||||||
|
isRefreshing: picture.isValidating,
|
||||||
|
picture: picture.data ?? null,
|
||||||
|
refresh,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export { assetsFactory } from "./assets.factory"
|
export { assetsFactory } from "./assets.factory"
|
||||||
export type {
|
export type {
|
||||||
AssetOverview,
|
AssetOverview,
|
||||||
|
AssetPicturePreview,
|
||||||
AssetsApi,
|
AssetsApi,
|
||||||
AssetsDashboard,
|
AssetsDashboard,
|
||||||
AssetVariantFormat,
|
AssetVariantFormat,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
|
AssetPictureResponseDto,
|
||||||
AssetVariantResponseDto,
|
AssetVariantResponseDto,
|
||||||
CreateAssetRequestDto,
|
CreateAssetRequestDto,
|
||||||
CreateAssetResponseDto,
|
CreateAssetResponseDto,
|
||||||
@@ -16,10 +17,21 @@ export type AssetVariantResize = "fill" | "fit"
|
|||||||
export type AssetOverview = {
|
export type AssetOverview = {
|
||||||
asset: AssetResponseDto | null
|
asset: AssetResponseDto | null
|
||||||
error?: Error
|
error?: Error
|
||||||
|
hasRunningVariants: boolean
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
|
isRefreshing: boolean
|
||||||
|
refresh: () => Promise<void>
|
||||||
variants: AssetVariantResponseDto[]
|
variants: AssetVariantResponseDto[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AssetPicturePreview = {
|
||||||
|
error?: Error
|
||||||
|
isLoading: boolean
|
||||||
|
isRefreshing: boolean
|
||||||
|
picture: AssetPictureResponseDto | null
|
||||||
|
refresh: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
export type AssetsDashboard = {
|
export type AssetsDashboard = {
|
||||||
allowedSourceHosts: string[]
|
allowedSourceHosts: string[]
|
||||||
assets: AssetResponseDto[]
|
assets: AssetResponseDto[]
|
||||||
@@ -77,6 +89,7 @@ export type GenerateAssetVariantsAction = {
|
|||||||
*/
|
*/
|
||||||
export type AssetsApi = {
|
export type AssetsApi = {
|
||||||
useAssetOverview: (publicId: string | null) => AssetOverview
|
useAssetOverview: (publicId: string | null) => AssetOverview
|
||||||
|
useAssetPicture: (publicId: string | null, preset: string | null) => AssetPicturePreview
|
||||||
useAssetsDashboard: () => AssetsDashboard
|
useAssetsDashboard: () => AssetsDashboard
|
||||||
useCreateAsset: () => CreateAssetAction
|
useCreateAsset: () => CreateAssetAction
|
||||||
useCreateAssetVersion: () => CreateAssetVersionAction
|
useCreateAssetVersion: () => CreateAssetVersionAction
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export { getAssetPictureKey, useGetAssetPicture } from "./use-get-asset-picture.hook"
|
||||||
|
export type { AssetPictureQuery } from "./use-get-asset-picture.hook"
|
||||||
export { getAssetKey, useGetAsset } from "./use-get-asset.hook"
|
export { getAssetKey, useGetAsset } from "./use-get-asset.hook"
|
||||||
export { getAssetVariantsKey, useGetAssetVariants } from "./use-get-asset-variants.hook"
|
export { getAssetVariantsKey, useGetAssetVariants } from "./use-get-asset-variants.hook"
|
||||||
export { getAssetsListKey, useGetAssetsList } from "./use-get-assets-list.hook"
|
export { getAssetsListKey, useGetAssetsList } from "./use-get-assets-list.hook"
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import useSWR from "swr"
|
||||||
|
import type { SWRConfiguration } from "swr"
|
||||||
|
|
||||||
|
import { backendApi } from "../client"
|
||||||
|
import type { AssetPictureResponseDto, GetAssetPictureParams } from "../generated/backend-api.generated"
|
||||||
|
|
||||||
|
export type AssetPictureQuery = Omit<GetAssetPictureParams, "publicId">
|
||||||
|
|
||||||
|
export const getAssetPictureKey = (publicId: string, query: AssetPictureQuery) =>
|
||||||
|
[
|
||||||
|
"backend-api",
|
||||||
|
"assets",
|
||||||
|
"picture",
|
||||||
|
publicId,
|
||||||
|
query.preset,
|
||||||
|
query.version ?? null,
|
||||||
|
query.quality ?? null,
|
||||||
|
query.sizes ?? null,
|
||||||
|
] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение picture/srcset contract asset.
|
||||||
|
*/
|
||||||
|
export const useGetAssetPicture = (
|
||||||
|
publicId: string | null,
|
||||||
|
query: AssetPictureQuery | null,
|
||||||
|
config?: SWRConfiguration<AssetPictureResponseDto>,
|
||||||
|
) => {
|
||||||
|
const key = publicId !== null && query !== null ? getAssetPictureKey(publicId, query) : null
|
||||||
|
const fetcher = () => backendApi.assets.getAssetPicture({ publicId: publicId ?? "", ...(query ?? { preset: "" }) })
|
||||||
|
|
||||||
|
return useSWR<AssetPictureResponseDto>(key, fetcher, config)
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export const getAssetVariantsKey = (publicId: string, version?: string) =>
|
|||||||
export const useGetAssetVariants = (
|
export const useGetAssetVariants = (
|
||||||
publicId: string | null,
|
publicId: string | null,
|
||||||
version?: string,
|
version?: string,
|
||||||
config?: SWRConfiguration,
|
config?: SWRConfiguration<AssetVariantsResponseDto>,
|
||||||
) => {
|
) => {
|
||||||
const key = publicId !== null ? getAssetVariantsKey(publicId, version) : null
|
const key = publicId !== null ? getAssetVariantsKey(publicId, version) : null
|
||||||
const fetcher = () => backendApi.assets.listAssetVariants({ publicId: publicId ?? "", version })
|
const fetcher = () => backendApi.assets.listAssetVariants({ publicId: publicId ?? "", version })
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { backendApi } from "./client"
|
|||||||
export * from "./hooks"
|
export * from "./hooks"
|
||||||
export type {
|
export type {
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
|
AssetPictureResponseDto,
|
||||||
AssetVariantResponseDto,
|
AssetVariantResponseDto,
|
||||||
AssetVariantsResponseDto,
|
AssetVariantsResponseDto,
|
||||||
AssetsListResponseDto,
|
AssetsListResponseDto,
|
||||||
@@ -12,6 +13,7 @@ export type {
|
|||||||
CreateAssetVariantsRequestDto,
|
CreateAssetVariantsRequestDto,
|
||||||
CreateAssetVariantsResponseDto,
|
CreateAssetVariantsResponseDto,
|
||||||
CustomTransformConfigResponseDto,
|
CustomTransformConfigResponseDto,
|
||||||
|
GetAssetPictureParams,
|
||||||
ListAssetsParams,
|
ListAssetsParams,
|
||||||
PresetResponseDto,
|
PresetResponseDto,
|
||||||
PresetsResponseDto,
|
PresetsResponseDto,
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export const DASHBOARD_CARDS = [
|
|||||||
|
|
||||||
export const DASHBOARD_PIPELINE = ["Backend", "RabbitMQ", "Worker", "imgproxy", "S3"] as const
|
export const DASHBOARD_PIPELINE = ["Backend", "RabbitMQ", "Worker", "imgproxy", "S3"] as const
|
||||||
|
|
||||||
|
export const DASHBOARD_ASSET_SEARCH_PARAM = "asset"
|
||||||
|
|
||||||
|
export const DASHBOARD_PRESET_SEARCH_PARAM = "preset"
|
||||||
|
|
||||||
export const ASSET_STATUS_COLORS = {
|
export const ASSET_STATUS_COLORS = {
|
||||||
active: "green",
|
active: "green",
|
||||||
deleted: "red",
|
deleted: "red",
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Alert, Button, Group, Paper, Stack, Text, Title } from "@mantine/core"
|
import { Alert, Button, Group, Paper, Stack, Text, Title } from "@mantine/core"
|
||||||
import { useDisclosure } from "@mantine/hooks"
|
import { useDisclosure } from "@mantine/hooks"
|
||||||
import cl from "clsx"
|
import cl from "clsx"
|
||||||
import { useState } from "react"
|
|
||||||
import { assetsFactory } from "business/assets"
|
import { assetsFactory } from "business/assets"
|
||||||
|
|
||||||
import { DASHBOARD_PIPELINE } from "./config/dashboard.config"
|
import { DASHBOARD_PIPELINE } from "./config/dashboard.config"
|
||||||
|
import { useDashboardUrlState } from "./hooks/use-dashboard-url-state.hook"
|
||||||
import { AssetDetailPanel } from "./parts/asset-detail-panel"
|
import { AssetDetailPanel } from "./parts/asset-detail-panel"
|
||||||
import { AssetsTable } from "./parts/assets-table"
|
import { AssetsTable } from "./parts/assets-table"
|
||||||
import { CreateAssetModal } from "./parts/create-asset-modal"
|
import { CreateAssetModal } from "./parts/create-asset-modal"
|
||||||
import { CreateSourceVersionModal } from "./parts/create-source-version-modal"
|
import { CreateSourceVersionModal } from "./parts/create-source-version-modal"
|
||||||
import { GenerateVariantsModal } from "./parts/generate-variants-modal"
|
import { GenerateVariantsModal } from "./parts/generate-variants-modal"
|
||||||
|
import { PicturePreviewPanel } from "./parts/picture-preview-panel"
|
||||||
import { PresetsPanel } from "./parts/presets-panel"
|
import { PresetsPanel } from "./parts/presets-panel"
|
||||||
import { SummaryCards } from "./parts/summary-cards"
|
import { SummaryCards } from "./parts/summary-cards"
|
||||||
import styles from "./styles/dashboard.module.css"
|
import styles from "./styles/dashboard.module.css"
|
||||||
@@ -26,7 +27,8 @@ const assets = assetsFactory()
|
|||||||
*/
|
*/
|
||||||
export const DashboardScreen = (props: DashboardScreenProps) => {
|
export const DashboardScreen = (props: DashboardScreenProps) => {
|
||||||
const { className, ...rootAttrs } = props
|
const { className, ...rootAttrs } = props
|
||||||
const [selectedPublicId, setSelectedPublicId] = useState<string | null>(null)
|
const { selectedPicturePreset, selectedPublicId, setSelectedPicturePreset, setSelectedPublicId } =
|
||||||
|
useDashboardUrlState()
|
||||||
const [isCreateAssetOpen, createAssetModal] = useDisclosure(false)
|
const [isCreateAssetOpen, createAssetModal] = useDisclosure(false)
|
||||||
const [isCreateVersionOpen, createVersionModal] = useDisclosure(false)
|
const [isCreateVersionOpen, createVersionModal] = useDisclosure(false)
|
||||||
const [isGenerateVariantsOpen, generateVariantsModal] = useDisclosure(false)
|
const [isGenerateVariantsOpen, generateVariantsModal] = useDisclosure(false)
|
||||||
@@ -35,7 +37,9 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
|||||||
const createAssetVersion = assets.useCreateAssetVersion()
|
const createAssetVersion = assets.useCreateAssetVersion()
|
||||||
const generateAssetVariants = assets.useGenerateAssetVariants()
|
const generateAssetVariants = assets.useGenerateAssetVariants()
|
||||||
const effectivePublicId = selectedPublicId ?? dashboard.assets[0]?.publicId ?? null
|
const effectivePublicId = selectedPublicId ?? dashboard.assets[0]?.publicId ?? null
|
||||||
|
const effectivePicturePreset = selectedPicturePreset ?? dashboard.presets[0]?.name ?? null
|
||||||
const overview = assets.useAssetOverview(effectivePublicId)
|
const overview = assets.useAssetOverview(effectivePublicId)
|
||||||
|
const picturePreview = assets.useAssetPicture(effectivePublicId, effectivePicturePreset)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section {...rootAttrs} className={cl(styles.root, className)}>
|
<section {...rootAttrs} className={cl(styles.root, className)}>
|
||||||
@@ -88,6 +92,14 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PicturePreviewPanel
|
||||||
|
onPresetChange={setSelectedPicturePreset}
|
||||||
|
picturePreview={picturePreview}
|
||||||
|
presets={dashboard.presets}
|
||||||
|
publicId={effectivePublicId}
|
||||||
|
selectedPreset={effectivePicturePreset}
|
||||||
|
/>
|
||||||
|
|
||||||
<PresetsPanel
|
<PresetsPanel
|
||||||
allowedSourceHosts={dashboard.allowedSourceHosts}
|
allowedSourceHosts={dashboard.allowedSourceHosts}
|
||||||
custom={dashboard.custom}
|
custom={dashboard.custom}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
import { DASHBOARD_ASSET_SEARCH_PARAM, DASHBOARD_PRESET_SEARCH_PARAM } from "../config/dashboard.config"
|
||||||
|
|
||||||
|
const readSearchParam = (name: string) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URLSearchParams(window.location.search).get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceSearchParam = (name: string, value: string | null) => {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
url.searchParams.set(name, value)
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL-state dashboard для выбранного asset и picture preset.
|
||||||
|
*/
|
||||||
|
export const useDashboardUrlState = () => {
|
||||||
|
const [selectedPublicId, setSelectedPublicIdState] = useState(() => readSearchParam(DASHBOARD_ASSET_SEARCH_PARAM))
|
||||||
|
const [selectedPicturePreset, setSelectedPicturePresetState] = useState(() =>
|
||||||
|
readSearchParam(DASHBOARD_PRESET_SEARCH_PARAM),
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopState = () => {
|
||||||
|
setSelectedPublicIdState(readSearchParam(DASHBOARD_ASSET_SEARCH_PARAM))
|
||||||
|
setSelectedPicturePresetState(readSearchParam(DASHBOARD_PRESET_SEARCH_PARAM))
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("popstate", handlePopState)
|
||||||
|
|
||||||
|
return () => window.removeEventListener("popstate", handlePopState)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setSelectedPublicId = (publicId: string | null) => {
|
||||||
|
setSelectedPublicIdState(publicId)
|
||||||
|
replaceSearchParam(DASHBOARD_ASSET_SEARCH_PARAM, publicId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSelectedPicturePreset = (preset: string | null) => {
|
||||||
|
setSelectedPicturePresetState(preset)
|
||||||
|
replaceSearchParam(DASHBOARD_PRESET_SEARCH_PARAM, preset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedPicturePreset,
|
||||||
|
selectedPublicId,
|
||||||
|
setSelectedPicturePreset,
|
||||||
|
setSelectedPublicId,
|
||||||
|
}
|
||||||
|
}
|
||||||
18
apps/admin/src/screens/dashboard/lib/copy-text.ts
Normal file
18
apps/admin/src/screens/dashboard/lib/copy-text.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { notifications } from "@mantine/notifications"
|
||||||
|
|
||||||
|
export const copyText = async (value: string, label: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value)
|
||||||
|
notifications.show({
|
||||||
|
color: "green",
|
||||||
|
message: `${label} скопирован в clipboard`,
|
||||||
|
title: "Copied",
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
notifications.show({
|
||||||
|
color: "red",
|
||||||
|
message: `Не удалось скопировать ${label}`,
|
||||||
|
title: "Copy failed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,21 @@
|
|||||||
import { Anchor, Badge, Button, Code, Group, Paper, ScrollArea, Skeleton, Stack, Table, Text, Title } from "@mantine/core"
|
import {
|
||||||
|
Anchor,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Code,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
ScrollArea,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core"
|
||||||
|
|
||||||
import { ASSET_STATUS_COLORS, VARIANT_STATUS_COLORS } from "../../config/dashboard.config"
|
import { ASSET_STATUS_COLORS, VARIANT_STATUS_COLORS } from "../../config/dashboard.config"
|
||||||
|
import { copyText } from "../../lib/copy-text"
|
||||||
import { formatDateTime } from "../../lib/format-date"
|
import { formatDateTime } from "../../lib/format-date"
|
||||||
import type { AssetDetailPanelProps } from "./types/asset-detail-panel-props.type"
|
import type { AssetDetailPanelProps } from "./types/asset-detail-panel-props.type"
|
||||||
|
|
||||||
@@ -48,9 +63,20 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
|||||||
<Button onClick={onGenerateVariants} radius="xl" size="xs">
|
<Button onClick={onGenerateVariants} radius="xl" size="xs">
|
||||||
Generate variants
|
Generate variants
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => void copyText(asset.publicId, "publicId")} radius="xl" size="xs" variant="subtle">
|
||||||
|
Copy ID
|
||||||
|
</Button>
|
||||||
|
<Button loading={overview.isRefreshing} onClick={overview.refresh} radius="xl" size="xs" variant="subtle">
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
<Badge color={ASSET_STATUS_COLORS[asset.status] ?? "gray"} radius="xl" variant="light">
|
<Badge color={ASSET_STATUS_COLORS[asset.status] ?? "gray"} radius="xl" variant="light">
|
||||||
{asset.status}
|
{asset.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{overview.hasRunningVariants ? (
|
||||||
|
<Badge color="yellow" radius="xl" variant="light">
|
||||||
|
polling variants
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
) : null}
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
@@ -63,9 +89,14 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
|||||||
<Text c="dimmed" fz="sm">
|
<Text c="dimmed" fz="sm">
|
||||||
Source URL
|
Source URL
|
||||||
</Text>
|
</Text>
|
||||||
<Anchor href={asset.sourceUrl} target="_blank">
|
<Group align="center" gap="xs">
|
||||||
{asset.sourceUrl}
|
<Anchor href={asset.sourceUrl} target="_blank">
|
||||||
</Anchor>
|
{asset.sourceUrl}
|
||||||
|
</Anchor>
|
||||||
|
<Button onClick={() => void copyText(asset.sourceUrl, "source URL")} radius="xl" size="compact-xs" variant="subtle">
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
@@ -95,16 +126,26 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
|||||||
<Table verticalSpacing="sm">
|
<Table verticalSpacing="sm">
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
|
<Table.Th>Preview</Table.Th>
|
||||||
<Table.Th>Preset</Table.Th>
|
<Table.Th>Preset</Table.Th>
|
||||||
<Table.Th>Format</Table.Th>
|
<Table.Th>Format</Table.Th>
|
||||||
<Table.Th>Size</Table.Th>
|
<Table.Th>Size</Table.Th>
|
||||||
<Table.Th>Status</Table.Th>
|
<Table.Th>Status</Table.Th>
|
||||||
<Table.Th>URL</Table.Th>
|
<Table.Th>URL</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{variants.map((variant) => (
|
{variants.map((variant) => (
|
||||||
<Table.Tr key={variant.id}>
|
<Table.Tr key={variant.id}>
|
||||||
|
<Table.Td>
|
||||||
|
{String(variant.status) === "ready" ? (
|
||||||
|
<Image alt={`${variant.preset} ${variant.format}`} fit="cover" h={46} radius="md" src={variant.url} w={72} />
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed" fz="xs">
|
||||||
|
not ready
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Code>{variant.preset}</Code>
|
<Code>{variant.preset}</Code>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
@@ -118,9 +159,14 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Anchor href={variant.url} target="_blank">
|
<Group gap="xs" wrap="nowrap">
|
||||||
open
|
<Anchor href={variant.url} target="_blank">
|
||||||
</Anchor>
|
open
|
||||||
|
</Anchor>
|
||||||
|
<Button onClick={() => void copyText(variant.url, "variant URL")} size="compact-xs" variant="subtle">
|
||||||
|
copy
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { PicturePreviewPanel } from "./picture-preview-panel"
|
||||||
|
export type { PicturePreviewPanelProps } from "./types/picture-preview-panel-props.type"
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { Anchor, Badge, Button, Code, Group, Image, Paper, Select, Skeleton, Stack, Table, Text, Title } from "@mantine/core"
|
||||||
|
|
||||||
|
import { copyText } from "../../lib/copy-text"
|
||||||
|
import type { PicturePreviewPanelProps } from "./types/picture-preview-panel-props.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview Backend picture/srcset contract.
|
||||||
|
*
|
||||||
|
* Используется для:
|
||||||
|
* - проверки consumer-facing picture contract
|
||||||
|
* - просмотра fallback image и generated srcset URLs
|
||||||
|
*/
|
||||||
|
export const PicturePreviewPanel = (props: PicturePreviewPanelProps) => {
|
||||||
|
const { onPresetChange, picturePreview, presets, publicId, selectedPreset } = props
|
||||||
|
const picture = picturePreview.picture
|
||||||
|
const presetOptions = presets.map((preset) => ({
|
||||||
|
label: `${preset.name} (${preset.mode})`,
|
||||||
|
value: preset.name,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper bg="white" p="xl" radius="xl" shadow="xs" withBorder>
|
||||||
|
<Group align="start" justify="space-between" mb="lg">
|
||||||
|
<div>
|
||||||
|
<Title order={2} size="h3">
|
||||||
|
Picture contract
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
Preview для `GET /api/assets/:publicId/picture`.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
loading={picturePreview.isRefreshing}
|
||||||
|
onClick={() => void picturePreview.refresh()}
|
||||||
|
radius="xl"
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Select
|
||||||
|
data={presetOptions}
|
||||||
|
disabled={!publicId || presetOptions.length === 0}
|
||||||
|
label="Preset"
|
||||||
|
onChange={onPresetChange}
|
||||||
|
placeholder="Select preset"
|
||||||
|
value={selectedPreset}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!publicId ? (
|
||||||
|
<Text c="dimmed">Выберите asset для preview.</Text>
|
||||||
|
) : picturePreview.isLoading ? (
|
||||||
|
<Skeleton height={280} radius="lg" />
|
||||||
|
) : picture ? (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Image alt={`${picture.publicId} ${picture.preset}`} fit="cover" mah={360} radius="lg" src={picture.image.src} />
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<Badge color="violet" radius="xl" variant="light">
|
||||||
|
{picture.preset}
|
||||||
|
</Badge>
|
||||||
|
<Badge radius="xl" variant="light">
|
||||||
|
v{picture.version}
|
||||||
|
</Badge>
|
||||||
|
<Badge radius="xl" variant="light">
|
||||||
|
q{picture.quality}
|
||||||
|
</Badge>
|
||||||
|
<Badge radius="xl" variant="light">
|
||||||
|
widths {picture.widths.join(", ")}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap={6}>
|
||||||
|
<Text c="dimmed" fz="sm" fw={700}>
|
||||||
|
Fallback URL
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Anchor href={picture.image.src} target="_blank">
|
||||||
|
{picture.image.src}
|
||||||
|
</Anchor>
|
||||||
|
<Button onClick={() => void copyText(picture.image.src, "fallback URL")} radius="xl" size="compact-xs" variant="subtle">
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Table verticalSpacing="sm">
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Format</Table.Th>
|
||||||
|
<Table.Th>Type</Table.Th>
|
||||||
|
<Table.Th>srcset</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{picture.sources.map((source) => (
|
||||||
|
<Table.Tr key={source.format}>
|
||||||
|
<Table.Td>
|
||||||
|
<Code>{source.format}</Code>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{source.type}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text lineClamp={2}>{source.srcSet}</Text>
|
||||||
|
<Button onClick={() => void copyText(source.srcSet, `${source.format} srcset`)} size="compact-xs" variant="subtle">
|
||||||
|
copy srcset
|
||||||
|
</Button>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed">Picture contract пока недоступен.</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { AssetPicturePreview, AssetsDashboard } from "business/assets"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры PicturePreviewPanel.
|
||||||
|
*/
|
||||||
|
export type PicturePreviewPanelProps = {
|
||||||
|
/** Preview contract `<picture>`. */
|
||||||
|
picturePreview: AssetPicturePreview
|
||||||
|
/** Public ID выбранного asset. */
|
||||||
|
publicId: string | null
|
||||||
|
/** Callback смены preset. */
|
||||||
|
onPresetChange: (preset: string | null) => void
|
||||||
|
/** Static presets. */
|
||||||
|
presets: AssetsDashboard["presets"]
|
||||||
|
/** Выбранный preset. */
|
||||||
|
selectedPreset: string | null
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user