|
|
@@ -15,6 +15,8 @@ export interface PluginInventorySettingsTabInjected {
|
|
|
}
|
|
|
|
|
|
type PluginInventoryEntry = PluginInventorySnapshot['entries'][number]
|
|
|
+type AgentPresetGroup = NonNullable<PluginInventorySnapshot['agentPresets']>[number]
|
|
|
+type AgentPresetRow = AgentPresetGroup['rows'][number]
|
|
|
type PluginFiberPhase = PluginInventoryEntry['fiberPhase']
|
|
|
|
|
|
/** Full component props assembled by the Settings slot renderer. */
|
|
|
@@ -23,6 +25,8 @@ export type PluginInventorySettingsTabProps =
|
|
|
& PropsLocale<'settings.pluginInventory'>
|
|
|
& InjectFace<PluginInventorySettingsTabInjected>
|
|
|
|
|
|
+type Translate = PluginInventorySettingsTabProps['t']
|
|
|
+
|
|
|
type ViewState =
|
|
|
| { readonly status: 'loading' }
|
|
|
| { readonly status: 'error' }
|
|
|
@@ -37,10 +41,7 @@ const PHASE_KEYS = {
|
|
|
} satisfies Record<Exclude<PluginFiberPhase, null>, PluginInventoryLocaleKey>
|
|
|
|
|
|
/** Localized accessible label for one root Fiber phase. */
|
|
|
-function phaseLabel(
|
|
|
- phase: PluginFiberPhase,
|
|
|
- t: PluginInventorySettingsTabProps['t'],
|
|
|
-): string {
|
|
|
+function phaseLabel(phase: PluginFiberPhase, t: Translate): string {
|
|
|
return phase === null ? t('unobserved') : t(PHASE_KEYS[phase])
|
|
|
}
|
|
|
|
|
|
@@ -53,19 +54,121 @@ function moduleShortName(moduleName: string): string {
|
|
|
.replace(/^dsh-(?:host-|client-)?/, '')
|
|
|
}
|
|
|
|
|
|
-/** Whether an inventory row matches the local catalog query. */
|
|
|
-function matches(entry: PluginInventoryEntry, normalizedQuery: string): boolean {
|
|
|
+/** Whether one row's module name or entry id matches the catalog query. */
|
|
|
+function matches(moduleName: string, entryId: string | null, normalizedQuery: string): boolean {
|
|
|
if (normalizedQuery.length === 0) return true
|
|
|
- return [entry.moduleName, entry.entryId]
|
|
|
+ return [moduleName, ...entryId === null ? [] : [entryId]]
|
|
|
.some(value => value.toLocaleLowerCase().includes(normalizedQuery))
|
|
|
}
|
|
|
|
|
|
-/** Render the read-only current Loader inventory. */
|
|
|
+/** The roster row shown when the preset switcher has no explicit choice. */
|
|
|
+function fallbackPreset(presets: readonly AgentPresetGroup[]): AgentPresetGroup | undefined {
|
|
|
+ return presets.find(preset => preset.isDefault) ?? presets[0]
|
|
|
+}
|
|
|
+
|
|
|
+/** The switcher's display label for one preset. */
|
|
|
+function presetLabel(preset: AgentPresetGroup, t: Translate): string {
|
|
|
+ const name = preset.name ?? preset.id
|
|
|
+ if (preset.broken !== undefined) return t('presetOptionBroken', { name })
|
|
|
+ if (preset.isDefault) return t('presetOptionDefault', { name })
|
|
|
+ return name
|
|
|
+}
|
|
|
+
|
|
|
+/** One expandable plugin card; the caller owns the trailing status content. */
|
|
|
+function PluginCard({ rowKey, moduleName, entryId, trailing, ariaLabel, failed, expanded, onToggle, children }: {
|
|
|
+ readonly rowKey: string
|
|
|
+ readonly moduleName: string
|
|
|
+ readonly entryId: string | null
|
|
|
+ readonly trailing: ReactNode
|
|
|
+ readonly ariaLabel: string
|
|
|
+ readonly failed: boolean
|
|
|
+ readonly expanded: string | null
|
|
|
+ readonly onToggle: (key: string) => void
|
|
|
+ readonly children: ReactNode
|
|
|
+}): ReactNode {
|
|
|
+ const open = expanded === rowKey
|
|
|
+ const detailId = `plugin-details-${encodeURIComponent(rowKey)}`
|
|
|
+ return (
|
|
|
+ <li
|
|
|
+ className={css.card}
|
|
|
+ data-plugin-entry={entryId ?? undefined}
|
|
|
+ data-plugin-module={moduleName}
|
|
|
+ data-failed={failed ? 'true' : undefined}
|
|
|
+ data-open={open ? 'true' : undefined}
|
|
|
+ >
|
|
|
+ <button
|
|
|
+ className={css.cardContent}
|
|
|
+ type="button"
|
|
|
+ aria-expanded={open}
|
|
|
+ aria-controls={detailId}
|
|
|
+ aria-label={ariaLabel}
|
|
|
+ onClick={() => { onToggle(rowKey) }}
|
|
|
+ >
|
|
|
+ <strong className={css.cardTitle} title={moduleName}>{moduleShortName(moduleName)}</strong>
|
|
|
+ <span className={css.cardTrailing}>
|
|
|
+ {trailing}
|
|
|
+ <IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
|
|
|
+ </span>
|
|
|
+ </button>
|
|
|
+ {open ? <div className={css.cardDetails} id={detailId}>{children}</div> : null}
|
|
|
+ </li>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+/** Detail rows shared by every card: the Loader identity, then labeled facts. */
|
|
|
+function CardFacts({ moduleName, moduleLabel, entryId, facts }: {
|
|
|
+ readonly moduleName: string
|
|
|
+ readonly moduleLabel: string
|
|
|
+ readonly entryId: string | null
|
|
|
+ readonly facts: readonly (readonly [label: string, value: ReactNode])[]
|
|
|
+}): ReactNode {
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ {entryId === null ? null : <code className={css.entryValue} data-loader-entry>{entryId}</code>}
|
|
|
+ <dl className={css.details}>
|
|
|
+ <div>
|
|
|
+ <dt>{moduleLabel}</dt>
|
|
|
+ <dd>{moduleName}</dd>
|
|
|
+ </div>
|
|
|
+ {facts.map(([label, value]) => (
|
|
|
+ <div key={label}>
|
|
|
+ <dt>{label}</dt>
|
|
|
+ <dd>{value}</dd>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </dl>
|
|
|
+ </>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+/** Status dot naming the root-fiber phase. */
|
|
|
+function PhaseDot({ phase, t }: { readonly phase: PluginFiberPhase; readonly t: Translate }): ReactNode {
|
|
|
+ const status = phaseLabel(phase, t)
|
|
|
+ return (
|
|
|
+ <span
|
|
|
+ className={css.statusDot}
|
|
|
+ data-phase={phase ?? 'unobserved'}
|
|
|
+ role="img"
|
|
|
+ aria-label={status}
|
|
|
+ title={status}
|
|
|
+ />
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+/** Enablement tag; `kind` selects the palette. */
|
|
|
+function StateTag({ kind, label }: { readonly kind: string; readonly label: string }): ReactNode {
|
|
|
+ return <span className={css.configTag} data-kind={kind}>{label}</span>
|
|
|
+}
|
|
|
+
|
|
|
+/** Render the read-only plugin inventory: agent presets first, then the global plane. */
|
|
|
export function PluginInventorySettingsTab({ list, t }: PluginInventorySettingsTabProps): ReactNode {
|
|
|
- const catalogId = useId()
|
|
|
+ const sectionId = useId()
|
|
|
const [request, setRequest] = useState(0)
|
|
|
const [query, setQuery] = useState('')
|
|
|
- const [expanded, setExpanded] = useState<PluginInventoryEntry['entryId'] | null>(null)
|
|
|
+ const [expanded, setExpanded] = useState<string | null>(null)
|
|
|
+ const [chosenPreset, setChosenPreset] = useState<string | null>(null)
|
|
|
+ const [globalOpen, setGlobalOpen] = useState<boolean | null>(null)
|
|
|
+ const [drawerOpen, setDrawerOpen] = useState(false)
|
|
|
const [state, setState] = useState<ViewState>({ status: 'loading' })
|
|
|
|
|
|
useEffect(() => {
|
|
|
@@ -78,23 +181,160 @@ export function PluginInventorySettingsTab({ list, t }: PluginInventorySettingsT
|
|
|
}, [list, request])
|
|
|
|
|
|
const normalizedQuery = query.trim().toLocaleLowerCase()
|
|
|
- const filteredEntries = useMemo(
|
|
|
- () => state.status === 'ready'
|
|
|
- ? state.snapshot.entries.filter(entry => matches(entry, normalizedQuery))
|
|
|
- : [],
|
|
|
- [normalizedQuery, state],
|
|
|
- )
|
|
|
+ const searching = normalizedQuery.length > 0
|
|
|
+ const snapshot = state.status === 'ready' ? state.snapshot : undefined
|
|
|
+ const presets = snapshot?.agentPresets ?? []
|
|
|
+ const selected = presets.find(preset => preset.id === chosenPreset) ?? fallbackPreset(presets)
|
|
|
|
|
|
- useEffect(() => {
|
|
|
- if (expanded !== null && !filteredEntries.some(entry => entry.entryId === expanded)) {
|
|
|
- setExpanded(null)
|
|
|
+ /** Presets that actually enable a module, keyed by module name. */
|
|
|
+ const enabledIn = useMemo(() => {
|
|
|
+ const found = new Map<string, [AgentPresetGroup, ...AgentPresetGroup[]]>()
|
|
|
+ for (const preset of presets) {
|
|
|
+ for (const row of preset.rows) {
|
|
|
+ if (row.enabled !== true) continue
|
|
|
+ const groups = found.get(row.moduleName)
|
|
|
+ if (groups === undefined) found.set(row.moduleName, [preset])
|
|
|
+ else if (!groups.includes(preset)) groups.push(preset)
|
|
|
+ }
|
|
|
}
|
|
|
- }, [expanded, filteredEntries])
|
|
|
+ return found
|
|
|
+ }, [presets])
|
|
|
+
|
|
|
+ const entries = snapshot?.entries ?? []
|
|
|
+ const failedEntries: PluginInventoryEntry[] = []
|
|
|
+ const drawerEntries: { entry: PluginInventoryEntry; providers: readonly [AgentPresetGroup, ...AgentPresetGroup[]] }[] = []
|
|
|
+ const regularEntries: PluginInventoryEntry[] = []
|
|
|
+ for (const entry of entries) {
|
|
|
+ const providers = enabledIn.get(entry.moduleName)
|
|
|
+ if (entry.fiberPhase === 'failed') failedEntries.push(entry)
|
|
|
+ else if (!entry.enabled && providers !== undefined) drawerEntries.push({ entry, providers })
|
|
|
+ else regularEntries.push(entry)
|
|
|
+ }
|
|
|
+
|
|
|
+ const entryMatch = (entry: PluginInventoryEntry): boolean => matches(entry.moduleName, entry.entryId, normalizedQuery)
|
|
|
+ const rowMatch = (row: AgentPresetRow): boolean => matches(row.moduleName, row.entryId, normalizedQuery)
|
|
|
+ const filteredFailed = failedEntries.filter(entryMatch)
|
|
|
+ const filteredDrawer = drawerEntries.filter(drawerRow => entryMatch(drawerRow.entry))
|
|
|
+ const filteredRegular = regularEntries.filter(entryMatch)
|
|
|
+ const globalCount = filteredFailed.length + filteredDrawer.length + filteredRegular.length
|
|
|
+ const selectedRows = selected === undefined ? [] : selected.rows.filter(rowMatch)
|
|
|
+ const otherPresetMatches = searching
|
|
|
+ ? presets.filter(preset => preset !== selected && preset.rows.some(rowMatch))
|
|
|
+ : []
|
|
|
+ const otherMatchCount = otherPresetMatches
|
|
|
+ .reduce((total, preset) => total + preset.rows.filter(rowMatch).length, 0)
|
|
|
+
|
|
|
+ const globalEffectiveOpen = searching || (globalOpen ?? presets.length === 0)
|
|
|
+ const drawerEffectiveOpen = searching || drawerOpen
|
|
|
+ const nothingMatches = searching && globalCount === 0 && selectedRows.length === 0
|
|
|
+ && otherPresetMatches.length === 0
|
|
|
|
|
|
const retry = (): void => {
|
|
|
setState({ status: 'loading' })
|
|
|
setRequest(value => value + 1)
|
|
|
}
|
|
|
+ const toggleRow = (key: string): void => {
|
|
|
+ setExpanded(current => current === key ? null : key)
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Trailing status and detail facts for one row of the selected preset. */
|
|
|
+ const presetRowCard = (preset: AgentPresetGroup, row: AgentPresetRow, index: number): ReactNode => {
|
|
|
+ const key = `preset:${preset.id}:${String(index)}`
|
|
|
+ const title = moduleShortName(row.moduleName)
|
|
|
+ const failed = row.fiberPhase === 'failed'
|
|
|
+ const stateText = failed
|
|
|
+ ? t('failedTag')
|
|
|
+ : row.enabled === true ? t('enabledTag') : row.enabled === false ? t('disabledTag') : t('conditionalTag')
|
|
|
+ const kind = failed ? 'failed' : row.enabled === true ? 'enabled' : row.enabled === false ? 'disabled' : 'conditional'
|
|
|
+ return (
|
|
|
+ <PluginCard
|
|
|
+ key={key}
|
|
|
+ rowKey={key}
|
|
|
+ moduleName={row.moduleName}
|
|
|
+ entryId={row.entryId}
|
|
|
+ failed={failed}
|
|
|
+ expanded={expanded}
|
|
|
+ onToggle={toggleRow}
|
|
|
+ ariaLabel={`${title}, ${stateText}`}
|
|
|
+ trailing={(
|
|
|
+ <>
|
|
|
+ {row.enabled === true && !failed ? <PhaseDot phase={row.fiberPhase} t={t} /> : null}
|
|
|
+ <StateTag kind={kind} label={stateText} />
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ <CardFacts
|
|
|
+ moduleName={row.moduleName}
|
|
|
+ moduleLabel={t('moduleLabel')}
|
|
|
+ entryId={row.entryId}
|
|
|
+ facts={[
|
|
|
+ [t('fromPreset'), preset.name ?? preset.id],
|
|
|
+ [t('configuration'), stateText],
|
|
|
+ ...row.fiberPhase === null ? [] : [[t('runtime'), phaseLabel(row.fiberPhase, t)] as const],
|
|
|
+ ...row.condition === undefined ? [] : [[t('condition'), <code key="condition">{row.condition}</code>] as const],
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ </PluginCard>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ /** One global-plane row; a drawer row carries the presets that enable it. */
|
|
|
+ const globalRowCard = (
|
|
|
+ entry: PluginInventoryEntry,
|
|
|
+ providers?: readonly [AgentPresetGroup, ...AgentPresetGroup[]],
|
|
|
+ ): ReactNode => {
|
|
|
+ const key = `${providers === undefined ? 'global' : 'drawer'}:${entry.entryId}`
|
|
|
+ const title = moduleShortName(entry.moduleName)
|
|
|
+ const failed = entry.fiberPhase === 'failed'
|
|
|
+ const stateText = failed
|
|
|
+ ? t('failedTag')
|
|
|
+ : providers !== undefined ? t('presetEnabledTag') : t(entry.enabled ? 'enabledTag' : 'disabledTag')
|
|
|
+ const kind = failed ? 'failed' : providers !== undefined ? 'preset' : entry.enabled ? 'enabled' : 'disabled'
|
|
|
+ return (
|
|
|
+ <PluginCard
|
|
|
+ key={key}
|
|
|
+ rowKey={key}
|
|
|
+ moduleName={entry.moduleName}
|
|
|
+ entryId={entry.entryId}
|
|
|
+ failed={failed}
|
|
|
+ expanded={expanded}
|
|
|
+ onToggle={toggleRow}
|
|
|
+ ariaLabel={`${title}, ${stateText}`}
|
|
|
+ trailing={(
|
|
|
+ <>
|
|
|
+ {entry.enabled && !failed ? <PhaseDot phase={entry.fiberPhase} t={t} /> : null}
|
|
|
+ <StateTag kind={kind} label={stateText} />
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ <CardFacts
|
|
|
+ moduleName={entry.moduleName}
|
|
|
+ moduleLabel={t('moduleLabel')}
|
|
|
+ entryId={entry.entryId}
|
|
|
+ facts={providers !== undefined
|
|
|
+ ? [
|
|
|
+ [t('configuration'), t('drawerDetail')],
|
|
|
+ [t('enabledIn'), (
|
|
|
+ <span className={css.enabledIn}>
|
|
|
+ <span>{providers.map(preset => preset.name ?? preset.id).join(' · ')}</span>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className={css.jumpLink}
|
|
|
+ onClick={() => { setChosenPreset(providers[0].id) }}
|
|
|
+ >
|
|
|
+ {t('viewInPreset')}
|
|
|
+ </button>
|
|
|
+ </span>
|
|
|
+ )],
|
|
|
+ ]
|
|
|
+ : [
|
|
|
+ [t('configuration'), t(entry.enabled ? 'enabledTag' : 'disabledTag')],
|
|
|
+ ...entry.enabled ? [[t('runtime'), phaseLabel(entry.fiberPhase, t)] as const] : [],
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ </PluginCard>
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
<div className={css.section} aria-busy={state.status === 'loading'}>
|
|
|
@@ -105,7 +345,7 @@ export function PluginInventorySettingsTab({ list, t }: PluginInventorySettingsT
|
|
|
<button type="button" onClick={retry}>{t('retry')}</button>
|
|
|
</div>
|
|
|
) : null}
|
|
|
- {state.status === 'ready' ? (
|
|
|
+ {snapshot !== undefined ? (
|
|
|
<div className={css.catalog}>
|
|
|
<label className={css.search}>
|
|
|
<IconSearchOutline16 aria-hidden="true" />
|
|
|
@@ -118,77 +358,102 @@ export function PluginInventorySettingsTab({ list, t }: PluginInventorySettingsT
|
|
|
onChange={(event) => { setQuery(event.currentTarget.value) }}
|
|
|
/>
|
|
|
</label>
|
|
|
- <div className={css.catalogHeading}>
|
|
|
- <h3>{t('catalog')}</h3>
|
|
|
- <span data-plugin-count={filteredEntries.length}>{filteredEntries.length}</span>
|
|
|
- </div>
|
|
|
- {state.snapshot.entries.length === 0 ? <p className={css.status}>{t('empty')}</p> : null}
|
|
|
- {state.snapshot.entries.length > 0 && filteredEntries.length === 0
|
|
|
- ? <p className={css.status}>{t('emptySearch')}</p>
|
|
|
- : null}
|
|
|
- {filteredEntries.length > 0 ? (
|
|
|
- <ul className={css.cards}>
|
|
|
- {filteredEntries.map((entry) => {
|
|
|
- const status = phaseLabel(entry.fiberPhase, t)
|
|
|
- const title = moduleShortName(entry.moduleName)
|
|
|
- const configuration = t(entry.enabled ? 'enabledTag' : 'disabledTag')
|
|
|
- const open = expanded === entry.entryId
|
|
|
- const detailId = `${catalogId}-details-${encodeURIComponent(entry.entryId)}`
|
|
|
- return (
|
|
|
- <li
|
|
|
- className={css.card}
|
|
|
- key={entry.entryId}
|
|
|
- data-plugin-entry={entry.entryId}
|
|
|
- data-open={open ? 'true' : undefined}
|
|
|
- >
|
|
|
+ {entries.length === 0 && presets.length === 0 ? <p className={css.status}>{t('empty')}</p> : null}
|
|
|
+ {nothingMatches ? <p className={css.status}>{t('emptySearch')}</p> : null}
|
|
|
+
|
|
|
+ {selected !== undefined ? (
|
|
|
+ <section className={css.group} data-plugin-scope="preset" data-preset-id={selected.id}>
|
|
|
+ <div className={css.groupHeader}>
|
|
|
+ <select
|
|
|
+ className={css.switcher}
|
|
|
+ aria-label={t('switcherLabel')}
|
|
|
+ value={selected.id}
|
|
|
+ onChange={(event) => { setChosenPreset(event.currentTarget.value) }}
|
|
|
+ >
|
|
|
+ {presets.map(preset => (
|
|
|
+ <option key={preset.id} value={preset.id}>{presetLabel(preset, t)}</option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ <span className={css.groupSubtitle}>{t('presetSubtitle')}</span>
|
|
|
+ <span className={css.groupCount} data-preset-plugin-count={selectedRows.length}>
|
|
|
+ {selectedRows.length}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ {selected.broken !== undefined ? (
|
|
|
+ <p className={css.brokenNote} role="alert">{selected.broken}</p>
|
|
|
+ ) : null}
|
|
|
+ {selectedRows.length > 0 ? (
|
|
|
+ <ul className={css.cards}>
|
|
|
+ {selectedRows.map((row, index) => presetRowCard(selected, row, index))}
|
|
|
+ </ul>
|
|
|
+ ) : null}
|
|
|
+ {otherMatchCount > 0 ? (
|
|
|
+ <p className={css.hint}>
|
|
|
+ {t('matchesInOtherPresets', { count: String(otherMatchCount) })}
|
|
|
+ {otherPresetMatches.map(preset => (
|
|
|
<button
|
|
|
- className={css.cardContent}
|
|
|
+ key={preset.id}
|
|
|
type="button"
|
|
|
- aria-expanded={open}
|
|
|
- aria-controls={detailId}
|
|
|
- aria-label={entry.enabled ? `${title}, ${status}, ${configuration}` : `${title}, ${configuration}`}
|
|
|
- onClick={() => {
|
|
|
- setExpanded(current => current === entry.entryId ? null : entry.entryId)
|
|
|
- }}
|
|
|
+ className={css.jumpLink}
|
|
|
+ onClick={() => { setChosenPreset(preset.id) }}
|
|
|
>
|
|
|
- <strong className={css.cardTitle} title={entry.moduleName}>{title}</strong>
|
|
|
- <span className={css.cardTrailing}>
|
|
|
- {entry.enabled ? (
|
|
|
- <span
|
|
|
- className={css.statusDot}
|
|
|
- data-phase={entry.fiberPhase ?? 'unobserved'}
|
|
|
- role="img"
|
|
|
- aria-label={status}
|
|
|
- title={status}
|
|
|
- />
|
|
|
- ) : null}
|
|
|
- <span className={css.configTag} data-enabled={entry.enabled ? 'true' : 'false'}>
|
|
|
- {configuration}
|
|
|
- </span>
|
|
|
- <IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
|
|
|
- </span>
|
|
|
+ {preset.name ?? preset.id}
|
|
|
</button>
|
|
|
- {open ? (
|
|
|
- <div className={css.cardDetails} id={detailId}>
|
|
|
- <code className={css.entryValue} data-loader-entry>{entry.entryId}</code>
|
|
|
- <dl className={css.details}>
|
|
|
- <div>
|
|
|
- <dt>{t('configuration')}</dt>
|
|
|
- <dd>{configuration}</dd>
|
|
|
- </div>
|
|
|
- {entry.enabled ? (
|
|
|
- <div>
|
|
|
- <dt>{t('cordis')}</dt>
|
|
|
- <dd>{status}</dd>
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
- </dl>
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
- </li>
|
|
|
- )
|
|
|
- })}
|
|
|
- </ul>
|
|
|
+ ))}
|
|
|
+ </p>
|
|
|
+ ) : null}
|
|
|
+ </section>
|
|
|
+ ) : null}
|
|
|
+
|
|
|
+ {entries.length > 0 ? (
|
|
|
+ <section className={css.group} data-plugin-scope="global">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className={css.groupToggle}
|
|
|
+ aria-expanded={globalEffectiveOpen}
|
|
|
+ aria-controls={`${sectionId}-global`}
|
|
|
+ onClick={() => { setGlobalOpen(!globalEffectiveOpen) }}
|
|
|
+ >
|
|
|
+ <IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
|
|
|
+ <span className={css.groupTitle}>{t('globalTitle')}</span>
|
|
|
+ <span className={css.groupSubtitle}>{t('globalSubtitle')}</span>
|
|
|
+ <span className={css.groupCount} data-plugin-count={globalCount}>{globalCount}</span>
|
|
|
+ {filteredFailed.length > 0 ? (
|
|
|
+ <span className={css.failedCount}>{filteredFailed.length} {t('failedCountLabel')}</span>
|
|
|
+ ) : null}
|
|
|
+ </button>
|
|
|
+ {globalEffectiveOpen ? (
|
|
|
+ <div id={`${sectionId}-global`}>
|
|
|
+ {filteredFailed.length + filteredRegular.length > 0 ? (
|
|
|
+ <ul className={css.cards}>
|
|
|
+ {filteredFailed.map(entry => globalRowCard(entry))}
|
|
|
+ {filteredRegular.map(entry => globalRowCard(entry))}
|
|
|
+ </ul>
|
|
|
+ ) : null}
|
|
|
+ {drawerEntries.length > 0 ? (
|
|
|
+ <div className={css.drawer} data-plugin-drawer>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className={css.groupToggle}
|
|
|
+ aria-expanded={drawerEffectiveOpen}
|
|
|
+ aria-controls={`${sectionId}-drawer`}
|
|
|
+ onClick={() => { setDrawerOpen(!drawerEffectiveOpen) }}
|
|
|
+ >
|
|
|
+ <IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
|
|
|
+ <span className={css.groupTitle}>{t('drawerTitle')}</span>
|
|
|
+ <span className={css.groupSubtitle}>{t('drawerSubtitle')}</span>
|
|
|
+ <span className={css.groupCount}>{filteredDrawer.length}</span>
|
|
|
+ </button>
|
|
|
+ {drawerEffectiveOpen && filteredDrawer.length > 0 ? (
|
|
|
+ <ul className={css.cards} id={`${sectionId}-drawer`}>
|
|
|
+ {filteredDrawer.map(drawerRow => globalRowCard(drawerRow.entry, drawerRow.providers))}
|
|
|
+ </ul>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </section>
|
|
|
) : null}
|
|
|
</div>
|
|
|
) : null}
|