diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 79a3c4f380c5..8ff4f1ebfd1b 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -616,7 +616,7 @@ }, "@@rules_rust+//crate_universe:extension.bzl%crate": { "general": { - "bzlTransitiveDigest": "t0LacG+6Y3sh8Y9kRThzg2d+viLv86e7ACVSHudAhlA=", + "bzlTransitiveDigest": "FBJ8rEFIAaJxNKbI8PBvdmePmLfLri38dvzRcNI+6RE=", "usagesDigest": "3QyfiyrJKYb+RnTA85xhz1ecQjw78S7PpkjVgmLoLxA=", "recordedFileInputs": { "@@//app/rust-ffi/Cargo.toml": "b17bf21f56720ffba5031fdf70296acb53ff018adcf17a8cdcec86b46103d1d8", diff --git a/app/gui/src/dashboard/components/AriaComponents/VisualTooltip/useVisualTooltip.tsx b/app/gui/src/dashboard/components/AriaComponents/VisualTooltip/useVisualTooltip.tsx index c2b47d5ce8e1..2cff8d4e5ed5 100644 --- a/app/gui/src/dashboard/components/AriaComponents/VisualTooltip/useVisualTooltip.tsx +++ b/app/gui/src/dashboard/components/AriaComponents/VisualTooltip/useVisualTooltip.tsx @@ -3,6 +3,7 @@ import * as aria from '#/components/aria' import * as ariaComponents from '#/components/AriaComponents' import Portal from '#/components/Portal' import * as eventCallback from '#/hooks/eventCallbackHooks' +import { unsafeWriteValue } from '#/utilities/write' import * as React from 'react' /** Props for {@link useVisualTooltip}. */ @@ -101,8 +102,13 @@ export function useVisualTooltip(props: VisualTooltipOptions): VisualTooltipRetu onHoverChange: handleHoverChange, }) + unsafeWriteValue(targetHoverProps, 'id', id) + return { - targetProps: aria.mergeProps>()(targetHoverProps, { id }), + // This is SAFE because we are writing the value to the targetHoverProps object + // above. + // eslint-disable-next-line no-restricted-syntax + targetProps: targetHoverProps as VisualTooltipReturn['targetProps'], tooltip: state.isOpen ? : null, - } as const + } } /** Props for {@link TooltipInner}. */ diff --git a/app/gui/src/dashboard/components/dashboard/AssetRow.tsx b/app/gui/src/dashboard/components/dashboard/AssetRow.tsx index e47130b75f0b..da0dd02b2437 100644 --- a/app/gui/src/dashboard/components/dashboard/AssetRow.tsx +++ b/app/gui/src/dashboard/components/dashboard/AssetRow.tsx @@ -408,7 +408,7 @@ export function RealAssetRow(props: RealAssetRowProps) { } }} className={tailwindMerge.twMerge( - 'h-table-row rounded-full transition-all ease-in-out rounded-rows-child [contain-intrinsic-size:44px] [content-visibility:auto]', + 'h-table-row rounded-full transition-all ease-in-out rounded-rows-child', visibility, (isDraggedOver || isSelected) && 'selected', )} diff --git a/app/gui/src/dashboard/components/dashboard/column/SharedWithColumn.tsx b/app/gui/src/dashboard/components/dashboard/column/SharedWithColumn.tsx index b336698c8321..9311db4999ce 100644 --- a/app/gui/src/dashboard/components/dashboard/column/SharedWithColumn.tsx +++ b/app/gui/src/dashboard/components/dashboard/column/SharedWithColumn.tsx @@ -24,7 +24,7 @@ export default function SharedWithColumn(props: SharedWithColumnPropsInternal) { const assetPermissions = item.permissions ?? [] return ( -
+
{(category.type === 'trash' ? assetPermissions.filter((permission) => permission.permission === PermissionAction.own) : assetPermissions diff --git a/app/gui/src/dashboard/utilities/LruCache.ts b/app/gui/src/dashboard/utilities/LruCache.ts new file mode 100644 index 000000000000..841e8f4cfb08 --- /dev/null +++ b/app/gui/src/dashboard/utilities/LruCache.ts @@ -0,0 +1,69 @@ +/** @file A simple LRU cache. */ +/** + * A simple LRU cache. + * + * Implementation based on https://github.com/dominictarr/hashlru#algorithm + */ +export class LRUCache { + private oldCache: Map + private cache: Map + + /** + * Create a new LRU cache. + */ + constructor(private readonly maxSize: number) { + this.cache = new Map() + this.oldCache = new Map() + } + + /** + * Get a value from the cache. + */ + get(key: K): V | undefined { + const newCacheValue = this.cache.get(key) + + if (newCacheValue != null) { + return newCacheValue + } + + const oldCacheValue = this.oldCache.get(key) + + if (oldCacheValue != null) { + this.cache.set(key, oldCacheValue) + this.evictIfNecessary() + } + + return oldCacheValue + } + + /** + * Set a value in the cache. + */ + set(key: K, value: V) { + const isValueInNewCache = this.cache.has(key) + + this.cache.set(key, value) + + if (isValueInNewCache) { + this.evictIfNecessary() + } + } + + /** + * Clear the cache. + */ + clear() { + this.cache.clear() + this.oldCache.clear() + } + + /** + * Evict the oldest value from the cache. + */ + private evictIfNecessary() { + if (this.cache.size > this.maxSize) { + this.oldCache = this.cache + this.cache = new Map() + } + } +} diff --git a/app/gui/src/dashboard/utilities/tailwindVariants.ts b/app/gui/src/dashboard/utilities/tailwindVariants.ts index 65908f4e64cb..82b3f035e6e4 100644 --- a/app/gui/src/dashboard/utilities/tailwindVariants.ts +++ b/app/gui/src/dashboard/utilities/tailwindVariants.ts @@ -3,12 +3,85 @@ import type { OmitUndefined } from 'tailwind-variants' import { createTV } from 'tailwind-variants' import { TAILWIND_MERGE_CONFIG } from '#/utilities/tailwindMerge' +import { LRUCache } from './LruCache' export * from 'tailwind-variants' +const MAX_CACHE_SIZE = 256 + +// eslint-disable-next-line no-restricted-syntax +const tvConstructor = createTV({ twMergeConfig: TAILWIND_MERGE_CONFIG }) + // This is a function, even though it does not contain function syntax. // eslint-disable-next-line no-restricted-syntax -export const tv = createTV({ twMergeConfig: TAILWIND_MERGE_CONFIG }) +export const tv: typeof tvConstructor = function tvWithLRU( + construct: Parameters[0], +) { + const cache = new LRUCache(MAX_CACHE_SIZE) + /** + * Get a cache key for a given set of arguments. + */ + function getCacheKey(props: Parameters[0]) { + return JSON.stringify(props) + } + + const baseVariants = tvConstructor(construct) + + /** + * Variants constructor with LRU cache. + */ + function variantsWithLRU(args: Parameters[0]) { + const cacheKey = getCacheKey(args) + const cached = cache.get(cacheKey) + + if (cached != null) { + return cached + } + + const result: unknown = baseVariants(args) + + if (typeof result === 'object' && result != null) { + for (const slot in result) { + // eslint-disable-next-line no-restricted-syntax + const value = result[slot as keyof typeof result] + + if (typeof value === 'function') { + const slotCachePrefix = slot + cacheKey + /** + * Wrap a slot function with a cache. + */ + // @ts-expect-error - This is a valid assignment. + result[slot] = function withSlotCache(props: Parameters[0]) { + const slotCacheKey = slotCachePrefix + getCacheKey(props) + const slotCache = cache.get(slotCacheKey) + + if (slotCache != null) { + return slotCache + } + + // @ts-expect-error - This is a valid assignment. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const classes = value(props) + + cache.set(slotCacheKey, classes) + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return classes + } + } + } + } + + cache.set(cacheKey, result) + return result + } + // Extend the prototype of the `variantsWithLRU` function with the `baseVariants` function. + // This is done to preserve the extra properties of the `baseVariants` function. + variantsWithLRU.__proto__ = baseVariants + + // eslint-disable-next-line no-restricted-syntax + return variantsWithLRU as unknown as typeof tvConstructor +} as unknown as typeof tvConstructor /** Extract function signatures from a type. */ export type ExtractFunction =