Commit e529efa6 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Make static sprite registry (#2615)

* make static sprite registry

* [skip ci] fix file stats command

* [skip ci] sort icons by name
parent ae759780
......@@ -16,6 +16,7 @@
/public/assets/configs
/public/icons/sprite.svg
/public/icons/sprite.*.svg
/public/icons/registry.json
/public/icons/README.md
/public/static/og_image.png
/public/sitemap.xml
......
......@@ -44,7 +44,7 @@ RUN yarn --frozen-lockfile --network-timeout 100000
# ****** STAGE 2: Build *******
# *****************************
FROM node:22.11.0-alpine AS builder
RUN apk add --no-cache --upgrade libc6-compat bash
RUN apk add --no-cache --upgrade libc6-compat bash jq
# pass build args to env variables
ARG GIT_COMMIT_SHA
......
#!/bin/bash
yarn icons build -i ./icons -o ./public/icons --optimize
icons_dir="./icons"
target_dir="./public/icons"
yarn icons build -i $icons_dir -o $target_dir --optimize
create_registry_file() {
# Create a temporary file to store the registry
local registry_file="$target_dir/registry.json"
# Start the JSON array
echo "[]" > "$registry_file"
# Detect OS and set appropriate stat command
get_file_size() {
local file="$1"
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
stat -f%z "$file"
else
# Linux and others
stat -c%s "$file"
fi
}
# Function to process each file
process_file() {
local file="$1"
local relative_path="${file#$icons_dir/}"
local file_size=$(get_file_size "$file")
# Create a temporary file with the new entry
jq --arg name "$relative_path" --arg size "$file_size" \
'. + [{"name": $name, "file_size": ($size|tonumber)}]' \
"$registry_file" > "${registry_file}.tmp"
# Move the temporary file back
mv "${registry_file}.tmp" "$registry_file"
}
# Find all SVG files and process them
find "$icons_dir" -type f -name "*.svg" | while read -r file; do
process_file "$file"
done
}
# Skip hash creation and renaming for playwright environment
if [ "$NEXT_PUBLIC_APP_ENV" != "pw" ]; then
# Generate hash from the sprite file
HASH=$(md5sum ./public/icons/sprite.svg | cut -d' ' -f1 | head -c 8)
HASH=$(md5sum $target_dir/sprite.svg | cut -d' ' -f1 | head -c 8)
# Remove old sprite files
rm -f ./public/icons/sprite.*.svg
rm -f $target_dir/sprite.*.svg
# Rename the new sprite file
mv ./public/icons/sprite.svg "./public/icons/sprite.${HASH}.svg"
mv $target_dir/sprite.svg "$target_dir/sprite.${HASH}.svg"
export NEXT_PUBLIC_ICON_SPRITE_HASH=${HASH}
# Skip registry creation in development environment
# just to make the dev build faster
# remove this condition if you want to create the registry file in development environment
if [ "$NEXT_PUBLIC_APP_ENV" != "development" ]; then
create_registry_file
fi
echo "SVG sprite created: sprite.${HASH}.svg"
else
echo "SVG sprite created: sprite.svg (hash skipped for playwright environment)"
......
......@@ -72,7 +72,6 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/api/csrf': 'Regular page',
'/api/healthz': 'Regular page',
'/api/config': 'Regular page',
'/api/sprite': 'Regular page',
};
export default function getPageOgType(pathname: Route['pathname']) {
......
......@@ -75,7 +75,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/api/csrf': DEFAULT_TEMPLATE,
'/api/healthz': DEFAULT_TEMPLATE,
'/api/config': DEFAULT_TEMPLATE,
'/api/sprite': DEFAULT_TEMPLATE,
};
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
......
......@@ -72,7 +72,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/api/csrf': '%network_name% node API CSRF token',
'/api/healthz': '%network_name% node API health check',
'/api/config': '%network_name% node API app config',
'/api/sprite': '%network_name% node API SVG sprite content',
};
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
......
......@@ -70,7 +70,6 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/api/csrf': 'Node API: CSRF token',
'/api/healthz': 'Node API: Health check',
'/api/config': 'Node API: App config',
'/api/sprite': 'Node API: SVG sprite content',
};
export default function getPageType(pathname: Route['pathname']) {
......
......@@ -26,7 +26,6 @@ declare module "nextjs-routes" {
| StaticRoute<"/api/metrics">
| StaticRoute<"/api/monitoring/invalid-api-schema">
| StaticRoute<"/api/proxy">
| StaticRoute<"/api/sprite">
| StaticRoute<"/api-docs">
| DynamicRoute<"/apps/[id]", { "id": string }>
| StaticRoute<"/apps">
......
import fs from 'fs';
import type { NextApiRequest, NextApiResponse } from 'next';
import path from 'path';
import config from 'configs/app';
const ROOT_DIR = './icons';
const NAME_PREFIX = ROOT_DIR.replace('./', '') + '/';
interface IconInfo {
name: string;
fileSize: number;
}
const getIconName = (filePath: string) => filePath.replace(NAME_PREFIX, '').replace('.svg', '');
function collectIconNames(dir: string) {
const files = fs.readdirSync(dir, { withFileTypes: true });
let icons: Array<IconInfo> = [];
files.forEach((file) => {
const filePath = path.join(dir, file.name);
const stats = fs.statSync(filePath);
file.name.endsWith('.svg') && icons.push({
name: getIconName(filePath),
fileSize: stats.size,
});
if (file.isDirectory()) {
icons = [ ...icons, ...collectIconNames(filePath) ];
}
});
return icons;
}
export default async function spriteHandler(req: NextApiRequest, res: NextApiResponse) {
if (!config.app.isDev) {
return res.status(404).json({ error: 'Not found' });
}
const icons = collectIconNames(ROOT_DIR);
res.status(200).json({
icons,
});
}
......@@ -2,9 +2,6 @@ import { Flex, Box, Tooltip, useClipboard, useColorModeValue } from '@chakra-ui/
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { StaticRoute } from 'nextjs-routes';
import { route } from 'nextjs-routes';
import useFetch from 'lib/hooks/useFetch';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -18,10 +15,10 @@ const formatFileSize = (fileSizeInBytes: number) => `${ (fileSizeInBytes / 1_024
interface IconInfo {
name: string;
fileSize: number;
file_size: number;
}
const Item = ({ name, fileSize, bgColor }: IconInfo & { bgColor: string }) => {
const Item = ({ name, file_size: fileSize, bgColor }: IconInfo & { bgColor: string }) => {
const { hasCopied, onCopy } = useClipboard(name, 1000);
const [ copied, setCopied ] = React.useState(false);
......@@ -44,7 +41,7 @@ const Item = ({ name, fileSize, bgColor }: IconInfo & { bgColor: string }) => {
onClick={ onCopy }
cursor="pointer"
>
<IconSvg name={ name as IconName } boxSize="100px" bgColor={ bgColor } borderRadius="base"/>
<IconSvg name={ name.replace('.svg', '') as IconName } boxSize="100px" bgColor={ bgColor } borderRadius="base"/>
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ copied }>
<Box fontWeight={ 500 } mt={ 2 }>{ name }</Box>
</Tooltip>
......@@ -61,8 +58,7 @@ const Sprite = () => {
const { data, isFetching, isError } = useQuery({
queryKey: [ 'sprite' ],
queryFn: () => {
const url = route({ pathname: '/node-api/sprite' as StaticRoute<'/api/sprite'>['pathname'] });
return fetch<{ icons: Array<IconInfo> }, unknown>(url);
return fetch<Array<IconInfo>, unknown>('/icons/registry.json');
},
});
......@@ -71,11 +67,13 @@ const Sprite = () => {
return <ContentLoader/>;
}
if (isError || !data || !('icons' in data)) {
if (isError || !data || !Array.isArray(data)) {
return <DataFetchAlert/>;
}
const items = data.icons.filter((icon) => icon.name.includes(searchTerm));
const items = data
.filter((icon) => icon.name.includes(searchTerm))
.sort((a, b) => a.name.localeCompare(b.name));
if (items.length === 0) {
return <EmptySearchResult text="No icons found"/>;
......@@ -89,12 +87,12 @@ const Sprite = () => {
})();
const total = React.useMemo(() => {
if (!data || !('icons' in data)) {
if (!data || !Array.isArray(data)) {
return;
}
return data?.icons.reduce((result, item) => {
return data.reduce((result, item) => {
result.num++;
result.fileSize += item.fileSize;
result.fileSize += item.file_size;
return result;
}, { num: 0, fileSize: 0 });
}, [ data ]);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment