diff --git a/packages/apps-config/src/endpoints/productionRelayKusama.ts b/packages/apps-config/src/endpoints/productionRelayKusama.ts index 031eb4b210d7..cf1d65a55e2f 100644 --- a/packages/apps-config/src/endpoints/productionRelayKusama.ts +++ b/packages/apps-config/src/endpoints/productionRelayKusama.ts @@ -957,6 +957,7 @@ export const prodParasKusamaCommon: EndpointOption[] = [ teleport: [-1], text: 'Coretime', ui: { + color: '#113911', logo: chainsCoretimeKusamaSVG } }, diff --git a/packages/apps-config/src/endpoints/testingRelayRococo.ts b/packages/apps-config/src/endpoints/testingRelayRococo.ts index 1fe94657bb01..a0ce9621fa80 100644 --- a/packages/apps-config/src/endpoints/testingRelayRococo.ts +++ b/packages/apps-config/src/endpoints/testingRelayRococo.ts @@ -720,7 +720,9 @@ export const testParasRococoCommon: EndpointOption[] = [ relayName: 'rococo', teleport: [-1], text: 'Coretime', - ui: {} + ui: { + color: '#f19135' + } }, { homepage: 'https://encointer.org/', diff --git a/packages/apps-config/src/endpoints/testingRelayWestend.ts b/packages/apps-config/src/endpoints/testingRelayWestend.ts index a8b81c151336..a97b958ca2ce 100644 --- a/packages/apps-config/src/endpoints/testingRelayWestend.ts +++ b/packages/apps-config/src/endpoints/testingRelayWestend.ts @@ -182,7 +182,9 @@ export const testParasWestendCommon: EndpointOption[] = [ relayName: 'westend', teleport: [-1], text: 'Coretime', - ui: {} + ui: { + color: '#f19135' + } }, { info: 'westendPeople', diff --git a/packages/apps-routing/src/broker.ts b/packages/apps-routing/src/broker.ts new file mode 100644 index 000000000000..adc8f7ca482c --- /dev/null +++ b/packages/apps-routing/src/broker.ts @@ -0,0 +1,22 @@ +// Copyright 2017-2024 @polkadot/apps-routing authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Route, TFunction } from './types.js'; + +import Component from '@polkadot/app-broker'; + +export default function create (t: TFunction): Route { + return { + Component, + display: { + needsApi: [ + 'query.broker.status' + ], + needsApiInstances: true + }, + group: 'network', + icon: 'flask', + name: 'broker', + text: t('nav.broker', 'Coretime Broker (Experimental)', { ns: 'app-broker' }) + }; +} diff --git a/packages/apps-routing/src/index.ts b/packages/apps-routing/src/index.ts index 32ee324a5442..60d3a739c24b 100644 --- a/packages/apps-routing/src/index.ts +++ b/packages/apps-routing/src/index.ts @@ -9,6 +9,7 @@ import alliance from './alliance.js'; import ambassador from './ambassador.js'; import assets from './assets.js'; import bounties from './bounties.js'; +import broker from './broker.js'; import calendar from './calendar.js'; import claims from './claims.js'; import collator from './collator.js'; @@ -61,6 +62,7 @@ export default function create (t: TFunction): Routes { // Legacy staking Pre v14 pallet version. stakingLegacy(t), collator(t), + broker(t), // governance v2 referenda(t), membership(t), diff --git a/packages/apps-routing/tsconfig.build.json b/packages/apps-routing/tsconfig.build.json index dae101945136..32327a5d919a 100644 --- a/packages/apps-routing/tsconfig.build.json +++ b/packages/apps-routing/tsconfig.build.json @@ -14,6 +14,7 @@ { "path": "../page-bounties/tsconfig.build.json" }, { "path": "../page-calendar/tsconfig.build.json" }, { "path": "../page-claims/tsconfig.build.json" }, + { "path": "../page-broker/tsconfig.build.json" }, { "path": "../page-collator/tsconfig.build.json" }, { "path": "../page-contracts/tsconfig.build.json" }, { "path": "../page-council/tsconfig.build.json" }, diff --git a/packages/apps/public/locales/en/app-broker.json b/packages/apps/public/locales/en/app-broker.json new file mode 100644 index 000000000000..61cb1d672816 --- /dev/null +++ b/packages/apps/public/locales/en/app-broker.json @@ -0,0 +1,31 @@ +{ + "All active/available cores": "All active/available cores", + "All available slices": "All available slices", + "All scehduled cores": "All scehduled cores", + "No core description found": "No core description found", + "No workload found": "No workload found", + "No workplan found": "No workplan found", + "Overview": "Overview", + "assignment": "assignment", + "broker Id": "broker Id", + "core": "core", + "core count": "core count", + "current timeslice": "current timeslice", + "current work": "current work", + "estimated bulk price": "estimated bulk price", + "mask": "mask", + "nav.broker": "Coretime Broker (Experimental)", + "next index": "next index", + "parachain id": "parachain id", + "pool size": "pool size", + "region length": "region length", + "selected core": "selected core", + "selected core for workload": "selected core for workload", + "selected core for workplan": "selected core for workplan", + "timeslice": "timeslice", + "timeslice period": "timeslice period", + "traffic": "traffic", + "work queue": "work queue", + "workload": "workload", + "workplan": "workplan" +} \ No newline at end of file diff --git a/packages/apps/public/locales/en/index.json b/packages/apps/public/locales/en/index.json index 14bf8b42b904..5a0d9bee5660 100644 --- a/packages/apps/public/locales/en/index.json +++ b/packages/apps/public/locales/en/index.json @@ -4,6 +4,7 @@ "app-alliance.json", "app-assets.json", "app-bounties.json", + "app-broker.json", "app-calendar.json", "app-claims.json", "app-collator.json", diff --git a/packages/apps/public/locales/en/translation.json b/packages/apps/public/locales/en/translation.json index b04d8ccdc4ea..ed585334dc7d 100644 --- a/packages/apps/public/locales/en/translation.json +++ b/packages/apps/public/locales/en/translation.json @@ -68,10 +68,13 @@ "Addresses": "", "Advanced creation options": "", "After delay": "", + "All active/available cores": "", "All active/available tracks": "", + "All available slices": "", "All bags": "", "All pools": "", "All rewards will go towards the selected output destination when a payout is made.": "", + "All scehduled cores": "", "All stashes": "", "All the listed validators and all their nominators will receive their rewards.": "", "All validators": "", @@ -429,6 +432,7 @@ "No committee proposals": "", "No completed campaigns found": "", "No contracts available": "", + "No core description found": "", "No council motions": "", "No discretionary lock-voting is in place; all DOT used to vote counts the same.": "", "No documentation provided": "", @@ -464,6 +468,8 @@ "No waiting validators found": "", "No websites": "", "No winners in this auction": "", + "No workload found": "", + "No workplan found": "", "No, block all nominations": "", "Node info": "", "Nominate": "", @@ -1166,6 +1172,7 @@ "asset name": "", "asset symbol": "", "assets": "", + "assignment": "", "at specific block": "", "auctions": "", "available signatories": "", @@ -1206,6 +1213,7 @@ "bounty remark": "", "bounty requested allocation": "", "bounty title": "", + "broker Id": "", "bytes": "", "bytes transferred": "", "calculated storage fee": "", @@ -1263,6 +1271,8 @@ "conviction": "", "conviction: Conviction": "", "copied": "", + "core": "", + "core count": "", "council candidates": "", "council proposal type": "", "count": "", @@ -1286,7 +1296,9 @@ "current range winning bid": "", "current support (failing)": "", "current support (passing)": "", + "current timeslice": "", "current value": "", + "current work": "", "currently elected": "", "custom endpoint": "", "decision deposit": "", @@ -1358,6 +1370,7 @@ "era {{era}}/unapplied": "", "eras": "", "errors": "", + "estimated bulk price": "", "ethereum private key": "", "event count": "", "events": "", @@ -1474,6 +1487,7 @@ "logs": "", "lowest / avg staked": "", "manage hardware connections": "", + "mask": "", "manage ledger app": "", "matches": "", "matrix name": "", @@ -1524,6 +1538,7 @@ "next": "", "next action": "", "next burn": "", + "next index": "", "no": "", "no addresses saved yet, add any existing address": "", "no name": "", @@ -1576,6 +1591,7 @@ "period": "", "points": "", "pool id": "", + "pool size": "", "pools": "", "pot": "", "preimage": "", @@ -1626,6 +1642,7 @@ "referendum id": "", "refresh in": "", "refund from account": "", + "region length": "", "register from": "", "registrar account": "", "registrar index": "", @@ -1661,6 +1678,9 @@ "seed (hex or string)": "", "select curator": "", "selected constant query": "", + "selected core": "", + "selected core for workload": "", + "selected core for workplan": "", "selected signatories": "", "selected state query": "", "selected track": "", @@ -1735,6 +1755,8 @@ "the supplied signature": "", "threshold": "", "timeout": "", + "timeslice": "", + "timeslice period": "", "tip": "", "tip amount": "", "tip reason": "", @@ -1757,6 +1779,8 @@ "total sub": "", "total transferable": "", "track origin": "", + "traffic": "", + "traffic multiplier": "", "transactions": "", "transfer asset": "", "transfer received": "", @@ -1841,6 +1865,9 @@ "with an index of": "", "with capacity": "", "with weight override": "", + "work queue": "", + "workload": "", + "workplan": "", "yes": "", "yesterday": "", "your current password": "", diff --git a/packages/apps/public/locales/es/translation.json b/packages/apps/public/locales/es/translation.json index 1bbf52b2d533..b802225c8df5 100644 --- a/packages/apps/public/locales/es/translation.json +++ b/packages/apps/public/locales/es/translation.json @@ -42,7 +42,10 @@ "Address Prefix": "Título de la dirección", "Adjust the mode from basic (with a limited number of beginner-user-friendly apps) to full (with all basic & advanced apps available)": "Ajustar el modo desde lo básico (con un número limitado de aplicaciones para principiantes) a lo más completo (con todas las aplicaciones básicas y avanzadas disponibles)", "Advanced creation options": "Opciones avanzadas para la creación", + "All active/available cores": "Todos los núcleos activos/disponibles", + "All available slices": "Todas las secciones disponibles", "All rewards will go towards the selected output destination when a payout is made.": "Todas las recompensas irán hacia el destino de salida seleccionado cuando se efectúe el pago.", + "All scehduled cores": "Todos los núcleos agendados", "All the listed validators and all their nominators will receive their rewards.": "Todos los validadores de la lista y todos sus nominadores recibirán su recompensa.", "Allocate a suggested tip amount. With enough endorsements, the suggested values are averaged and sent to the beneficiary.": "Asigna una cantidad sugerida de propina. Con suficiente respaldo, los valores sugeridos serán un promedio y se enviarán al beneficiario.", "Amount to add to the currently bonded funds. This is adjusted using the available funds on the account.": "Cantidad a añadir a los fondos actualmente en reserva. Se ajusta con los fondos disponibles en la cuenta.", @@ -241,6 +244,7 @@ "No code hashes available": "No hay hash de código disponible", "No committee proposals": "No hay propuestas del comité", "No contracts available": "No hay contrato disponible", + "No core description found": "No se ha encontrado descripción de núcleos", "No council motions": "No hay mociones del consejo", "No documentation provided": "No se ha facilitado documentación", "No events available": "No hay eventos disponibles", @@ -259,6 +263,8 @@ "No runners up found": "No se han encontrado mensajeros", "No upgradable extensions found": "No se han encontrado extensiones actualizables", "No waiting validators found": "No se han encontrado validadores en espera", + "No workload found": "No se ha encontrado workload", + "No workplan found": "No se ha encontrado workplan", "Node info": "Información del nodo", "Nominate": "Nominar", "Nominate Validators": "Nominar validadores", @@ -708,6 +714,7 @@ "approval type": "tipo de aprobación", "approved": "aprobado", "asset id": "id del activo", + "assignment": "asignación", "auto-selected targets for nomination": "candidatos auto seleccionados para la nominación", "available": "disponible", "available signatories": "firmantes disponibles", @@ -729,6 +736,7 @@ "blocks": "bloques", "bond": "vínculo", "bonded": "vinculado", + "broker Id": "Id del Broker", "call from account": "llamada desde la cuenta", "call the selected endpoint": "llamar al punto final seleccionado", "candidate account": "cuenta del candidato", @@ -755,12 +763,16 @@ "conviction": "sentencia", "conviction: Conviction": "convicción: Convicción", "copied": "copiado", + "core": "núcleo", + "core count": "cantidad de núcleos", "council candidates": "candidatos al consejo", "council proposal": "propuesta del consejo", "council proposal type": "tipo de propuesta del consejo", "created account": "cuenta creada", "created multisig": "multisig creada", "crypto type to use": "typo de crypto a usar", + "current timeslice": "rango de tiempo actual", + "current work": "trabajo actual", "custom endpoint": "endpoint personalizado", "data": "dato", "default icon theme": "tema del icono por defecto", @@ -841,6 +853,7 @@ "locked balance": "balance bloqueado", "logs": "logs", "manage hardware connections": "manejar conexiones hardware", + "mask": "máscara", "matches": "coincidencias", "maximum gas allowed": "máximo de gas permitido", "members": "miembros", @@ -883,6 +896,7 @@ "new address": "nueva dirección", "next": "siguiente", "next id": "siguiente ID", + "next index": "next index", "no": "no", "no accounts yet, create or import an existing": "no hay cuentas aún, cree o importe un existente", "no addresses saved yet, add any existing address": "no hay direcciones almacenadas aún, añada cualquier dirección existente", @@ -916,6 +930,7 @@ "pending hashes": "hashes pendientes", "pending swap id": "pendiente del ID de intercambio", "points": "puntos", + "pool size": "Tamaño de pool", "pot": "espacio", "preimage hash": "hash de la preimgaen", "prev": "previo", @@ -983,6 +998,8 @@ "seed (hex or string)": "semilla (hexadecimal o secuencia)", "select the account you wish to sign data with": "seleccionar la cuenta que desea para firmar el dato", "selected constant query": "consulta constante seleccionada", + "selected core for workload": "núcleo seleccionado para workload", + "selected core for workplan": "núcleo seleccionado para workplan", "selected signatories": "firmantes seleccionados", "selected state query": "consulta de estado seleccionado", "selected validators": "validadores elegidos", @@ -1050,6 +1067,7 @@ "total peers": "pares totales", "total stake": "stake total", "total staked": "staked total", + "traffic": "tráfico", "transactions": "transacciones", "transfer received": "transferencia recibida", "transferable": "transferible", @@ -1095,6 +1113,8 @@ "web": "web", "website": "sitio web", "with an index of": "con el índice de", + "work queue": "cola de trabajo", + "workload": "workload", "wrong password supplied": "contraseña incorrecta suministrada", "yes": "si", "your current password": "su contraseña actual", diff --git a/packages/page-broker/.skip-build b/packages/page-broker/.skip-build new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/page-broker/.skip-npm b/packages/page-broker/.skip-npm new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/page-broker/README.md b/packages/page-broker/README.md new file mode 100644 index 000000000000..18165844eb42 --- /dev/null +++ b/packages/page-broker/README.md @@ -0,0 +1 @@ +# @polkadot/app-broker diff --git a/packages/page-broker/package.json b/packages/page-broker/package.json new file mode 100644 index 000000000000..1e1c742a1472 --- /dev/null +++ b/packages/page-broker/package.json @@ -0,0 +1,27 @@ +{ + "bugs": "https://github.com/polkadot-js/apps/issues", + "engines": { + "node": ">=18" + }, + "homepage": "https://github.com/polkadot-js/apps/tree/master/packages/page-broker#readme", + "license": "Apache-2.0", + "name": "@polkadot/app-broker", + "private": true, + "repository": { + "directory": "packages/page-broker", + "type": "git", + "url": "https://github.com/polkadot-js/apps.git" + }, + "sideEffects": false, + "type": "module", + "version": "0.143.3-21-x", + "dependencies": { + "@polkadot/react-components": "0.143.3-21-x", + "@polkadot/react-query": "0.143.3-21-x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*", + "react-is": "*" + } +} diff --git a/packages/page-broker/src/Overview/CoreTable.tsx b/packages/page-broker/src/Overview/CoreTable.tsx new file mode 100644 index 000000000000..2882ec625668 --- /dev/null +++ b/packages/page-broker/src/Overview/CoreTable.tsx @@ -0,0 +1,54 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; + +import React, { useRef } from 'react'; + +import { Table } from '@polkadot/react-components'; + +import { useTranslation } from '../translate.js'; +import { type CoreWorkloadType, type CoreWorkplanType } from '../types.js'; +import Workload from './Workload.js'; + +interface Props { + api: ApiPromise; + core: number; + workload?: CoreWorkloadType[], + workplan?: CoreWorkplanType[], + timeslice: number, +} + +function CoreTable ({ api, core, timeslice, workload, workplan }: Props): React.ReactElement { + const { t } = useTranslation(); + const headerRef = useRef<([React.ReactNode?, string?] | false)[]>([[t('core')]]); + const header: [React.ReactNode?, string?, number?, (() => void)?][] = [ + [ +
{headerRef.current} {core}
, + 'core', + 8, + undefined + ] + ]; + + return ( + + {workload?.map((v) => ( + + ))} +
+ ); +} + +export default React.memo(CoreTable); diff --git a/packages/page-broker/src/Overview/CoresTables.tsx b/packages/page-broker/src/Overview/CoresTables.tsx new file mode 100644 index 000000000000..4290b567ddab --- /dev/null +++ b/packages/page-broker/src/Overview/CoresTables.tsx @@ -0,0 +1,47 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { CoreInfo, CoreWorkloadType, CoreWorkplanType } from '../types.js'; + +import React, { useMemo } from 'react'; + +import CoreTable from './CoreTable.js'; + +interface Props { + api: ApiPromise; + workloadInfos?: CoreWorkloadType[]; + workplanInfos?: CoreWorkplanType[]; + timeslice: number; +} + +function CoresTable ({ api, timeslice, workloadInfos, workplanInfos }: Props): React.ReactElement { + const coreArr: number[] = useMemo(() => workloadInfos ? [...workloadInfos.map((plan) => plan.core)] : [], [workloadInfos]); + const filteredList: CoreInfo[] = useMemo(() => coreArr.map((c) => ({ + core: c, + workload: workloadInfos?.filter((v) => v.core === c), + workplan: workplanInfos?.filter((v) => v.core === c) + })), [workloadInfos, workplanInfos, coreArr]); + + return ( + <> + { + filteredList.map((c) => { + return ( + + ); + } + ) + } + + ); +} + +export default React.memo(CoresTable); diff --git a/packages/page-broker/src/Overview/Filters.tsx b/packages/page-broker/src/Overview/Filters.tsx new file mode 100644 index 000000000000..c0c1dbb8febf --- /dev/null +++ b/packages/page-broker/src/Overview/Filters.tsx @@ -0,0 +1,105 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { Dropdown, Input, styled } from '@polkadot/react-components'; +import { useDebounce } from '@polkadot/react-hooks'; + +import { useTranslation } from '../translate.js'; +import { type CoreWorkloadType, type CoreWorkplanType } from '../types.js'; + +const StyledDiv = styled.div` + @media (max-width: 768px) { + max-width: 100%: + } +`; + +interface Props { + workLoad?: CoreWorkloadType[]; + onFilter: (data: CoreWorkloadType[]) => void +} + +const filterLoad = (parachainId: string, load: CoreWorkloadType[] | CoreWorkplanType[], workloadCoreSelected: number): CoreWorkloadType[] | CoreWorkplanType[] => { + if (parachainId) { + return load?.filter(({ info }) => info.task === parachainId); + } + + if (workloadCoreSelected === -1) { + return load; + } + + return load?.filter(({ core }) => core === workloadCoreSelected); +}; + +function Filters ({ onFilter, workLoad }: Props): React.ReactElement { + const [workloadCoreSelected, setWorkloadCoreSelected] = useState(-1); + const [_parachainId, setParachainId] = useState(''); + + const coreArr: number[] = useMemo(() => + workLoad?.length + ? Array.from({ length: workLoad?.length || 0 }, (_, index) => index) + : [] + , [workLoad]); + + const { t } = useTranslation(); + const parachainId = useDebounce(_parachainId); + + const workloadCoreOpts = useMemo( + () => coreArr && [{ text: t('All active/available cores'), value: -1 }].concat( + coreArr + .map((c) => ( + { + text: `Core ${c}`, + value: c + } + )) + .filter((v): v is { text: string, value: number } => !!v.text) + ), + [coreArr, t] + ); + + useEffect(() => { + if (!workLoad) { + return; + } + + const filtered = filterLoad(parachainId, workLoad, workloadCoreSelected); + + onFilter(filtered); + }, [workLoad, workloadCoreSelected, parachainId, onFilter]); + + const onDropDownChange = useCallback((v: number) => { + setWorkloadCoreSelected(v); + setParachainId(''); + }, []); + + const onInputChange = useCallback((v: string) => { + setParachainId(v); + setWorkloadCoreSelected(-1); + }, []); + + return ( + + +
+ +
+
+ + ); +} + +export default React.memo(Filters); diff --git a/packages/page-broker/src/Overview/Summary.tsx b/packages/page-broker/src/Overview/Summary.tsx new file mode 100644 index 000000000000..8f369ea85585 --- /dev/null +++ b/packages/page-broker/src/Overview/Summary.tsx @@ -0,0 +1,93 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LinkOption } from '@polkadot/apps-config/endpoints/types'; +import type { CoreWorkloadType, statsType } from '../types.js'; + +import React from 'react'; + +import { CardSummary, styled, SummaryBox, UsageBar } from '@polkadot/react-components'; +import { defaultHighlight } from '@polkadot/react-components/styles'; +import { useApi, useBrokerStatus, useCurrentPrice, useRenewalBump } from '@polkadot/react-hooks'; + +import { useTranslation } from '../translate.js'; +import { getStats } from '../utils.js'; +import Cores from './Summary/Cores.js'; +import RegionLength from './Summary/RegionLength.js'; +import RenewalPrice from './Summary/RenewalPrice.js'; +import Timeslice from './Summary/Timeslice.js'; +import TimeslicePeriod from './Summary/TimeslicePeriod.js'; + +const StyledDiv = styled.div` + display: flex; + flex-wrap: wrap; + gap: 1rem; +`; + +const StyledSection = styled.section` + display: flex; + gap: 1rem; + @media (max-width: 768px) { + flex-direction: column; + margin-bottom: 2rem + } +`; + +interface Props { + apiEndpoint?: LinkOption | null; + workloadInfos?: CoreWorkloadType[] | CoreWorkloadType +} + +function Summary ({ workloadInfos }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api, apiEndpoint } = useApi(); + const renewalBump = useRenewalBump(); + const currentPrice = useCurrentPrice(); + const totalCores = useBrokerStatus('coreCount'); + const uiHighlight = apiEndpoint?.ui.color || defaultHighlight; + const { idles, pools, tasks }: statsType = React.useMemo(() => getStats(totalCores, workloadInfos), [totalCores, workloadInfos]); + + return ( + + + {api.query.broker && ( + <> + + + + + + + + + + + + + + + + + + + + )} +
+ + +
+
+
+ ); +} + +export default React.memo(Summary); diff --git a/packages/page-broker/src/Overview/Summary/Cores.tsx b/packages/page-broker/src/Overview/Summary/Cores.tsx new file mode 100644 index 000000000000..2e15228a741d --- /dev/null +++ b/packages/page-broker/src/Overview/Summary/Cores.tsx @@ -0,0 +1,24 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { BrokerStatus } from '@polkadot/react-query'; + +interface Props { + children?: React.ReactNode; + className?: string; +} + +function Cores ({ children, className }: Props): React.ReactElement | null { + return ( + + {children} + + ); +} + +export default React.memo(Cores); diff --git a/packages/page-broker/src/Overview/Summary/RegionLength.tsx b/packages/page-broker/src/Overview/Summary/RegionLength.tsx new file mode 100644 index 000000000000..130010659b87 --- /dev/null +++ b/packages/page-broker/src/Overview/Summary/RegionLength.tsx @@ -0,0 +1,28 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PalletBrokerConfigRecord } from '@polkadot/types/lookup'; + +import React from 'react'; + +import { useApi, useCall } from '@polkadot/react-hooks'; + +interface Props { + className?: string; + children?: React.ReactNode; +} + +function RegionLength ({ children, className }: Props): React.ReactElement | null { + const { api } = useApi(); + const config = useCall(api.query.broker?.configuration); + const length = config?.toJSON().regionLength; + + return ( +
+ {length?.toString()} + {children} +
+ ); +} + +export default React.memo(RegionLength); diff --git a/packages/page-broker/src/Overview/Summary/RenewalPrice.tsx b/packages/page-broker/src/Overview/Summary/RenewalPrice.tsx new file mode 100644 index 000000000000..df34e3d1bc4c --- /dev/null +++ b/packages/page-broker/src/Overview/Summary/RenewalPrice.tsx @@ -0,0 +1,30 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { formatBalance } from '@polkadot/util'; + +interface Props { + className?: string; + children?: React.ReactNode; + renewalBump?: string; + currentPrice?: string; +} + +function RenewalPrice ({ currentPrice, renewalBump }: Props): React.ReactElement | null { + const percentage = renewalBump ? Number.parseInt(renewalBump) / 1000000000 : 0; + + const price = currentPrice ? Number.parseInt(currentPrice) : 0; + + const renewalPrice = price * percentage + price; + + return ( +
+ {formatBalance(renewalPrice)} +
+ + ); +} + +export default React.memo(RenewalPrice); diff --git a/packages/page-broker/src/Overview/Summary/Timeslice.tsx b/packages/page-broker/src/Overview/Summary/Timeslice.tsx new file mode 100644 index 000000000000..a8f3546d3bee --- /dev/null +++ b/packages/page-broker/src/Overview/Summary/Timeslice.tsx @@ -0,0 +1,24 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { BrokerStatus } from '@polkadot/react-query'; + +interface Props { + children?: React.ReactNode; + className?: string; +} + +function Timeslice ({ children, className }: Props): React.ReactElement | null { + return ( + + {children} + + ); +} + +export default React.memo(Timeslice); diff --git a/packages/page-broker/src/Overview/Summary/TimeslicePeriod.tsx b/packages/page-broker/src/Overview/Summary/TimeslicePeriod.tsx new file mode 100644 index 000000000000..2581778ae1a4 --- /dev/null +++ b/packages/page-broker/src/Overview/Summary/TimeslicePeriod.tsx @@ -0,0 +1,27 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { u32 } from '@polkadot/types'; + +import React from 'react'; + +import { useApi } from '@polkadot/react-hooks'; + +interface Props { + className?: string; + children?: React.ReactNode; +} + +function BrokerId ({ children, className }: Props): React.ReactElement | null { + const { api } = useApi(); + const period = api.consts.broker?.timeslicePeriod as u32; + + return ( +
+ {period?.toString()} + {children} +
+ ); +} + +export default React.memo(BrokerId); diff --git a/packages/page-broker/src/Overview/WorkInfoRow.tsx b/packages/page-broker/src/Overview/WorkInfoRow.tsx new file mode 100644 index 000000000000..19a806520e4c --- /dev/null +++ b/packages/page-broker/src/Overview/WorkInfoRow.tsx @@ -0,0 +1,141 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { AddressMini, styled } from '@polkadot/react-components'; + +import { type InfoRow, Occupancy } from '../types.js'; + +const StyledTableCol = styled.td<{ hide?: 'mobile' | 'tablet' | 'both' }>` + width: 150px; + vertical-align: top; + + @media (max-width: 768px) { + /* Mobile */ + ${(props) => props.hide === 'mobile' || props.hide === 'both' ? 'display: none;' : ''} + } + + @media (min-width: 769px) and (max-width: 1024px) { + /* Tablet */ + ${(props) => props.hide === 'tablet' || props.hide === 'both' ? 'display: none;' : ''} + } +`; + +const TableCol = ({ header, + hide, + value }: { + header: string; + value: string | number | null | undefined; + hide?: 'mobile' | 'tablet' | 'both'; +}) => ( + +
{header}
+

{value || <> }

+
+); + +function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement { + if (!data.task) { + return ( + <> + no task assigned + + ); + } + + switch (data.type) { + case (Occupancy.Reservation): { + return ( + <> + + + + + + ); + } + + case (Occupancy.Lease): { + return ( + <> + + + + + + + ); + } + + default: { + return <> + + + + + + +
{'Owner'}
+ {data.owner + ? ( + + ) + + :

 

} +
; + } + } +} + +export default React.memo(WorkInfoRow); diff --git a/packages/page-broker/src/Overview/Workload.tsx b/packages/page-broker/src/Overview/Workload.tsx new file mode 100644 index 000000000000..889992cd4667 --- /dev/null +++ b/packages/page-broker/src/Overview/Workload.tsx @@ -0,0 +1,85 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { RegionInfo } from '@polkadot/react-hooks/types'; +import type { CoreWorkloadType, CoreWorkplanType, InfoRow } from '../types.js'; + +import React, { useEffect, useState } from 'react'; + +import { ExpandButton } from '@polkadot/react-components'; +import { useRegions, useToggle } from '@polkadot/react-hooks'; + +import { formatRowInfo } from '../utils.js'; +import WorkInfoRow from './WorkInfoRow.js'; +import Workplan from './Workplan.js'; + +interface Props { + api: ApiPromise; + value: CoreWorkloadType + timeslice: number; + workplan?: CoreWorkplanType[] | null +} + +function Workload ({ api, timeslice, value: { core, info, lastBlock, type }, workplan }: Props): React.ReactElement { + const [isExpanded, toggleIsExpanded] = useToggle(false); + const [tableData, setTableData] = useState(); + const [currentRegion, setCurrentRegion] = useState(); + const regionInfo = useRegions(api); + + useEffect(() => { + if (info) { + const region: RegionInfo | undefined = regionInfo?.find((v) => v.core === core && v.start <= timeslice && v.end > timeslice); + + setTableData(formatRowInfo(info, core, region, timeslice, type, lastBlock)); + setCurrentRegion(region); + } + }, [info, regionInfo, core, timeslice, lastBlock, type]); + + const hasWorkplan = !!workplan?.length; + + return ( + <> + {tableData && + + + +
Workplan ({workplan?.length})
+ {hasWorkplan && + ( + + ) + } + {!hasWorkplan && 'none'} + + + } + {isExpanded && + <> + + workplans + + + {workplan?.map((workplanInfo) => ( + + ))} + + + } + + ); +} + +export default React.memo(Workload); diff --git a/packages/page-broker/src/Overview/Workplan.tsx b/packages/page-broker/src/Overview/Workplan.tsx new file mode 100644 index 000000000000..e5a19f35e280 --- /dev/null +++ b/packages/page-broker/src/Overview/Workplan.tsx @@ -0,0 +1,56 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { RegionInfo } from '@polkadot/react-hooks/types'; +import type { CoreWorkplanType, InfoRow } from '../types.js'; + +import React, { useMemo } from 'react'; + +import { Spinner } from '@polkadot/react-components'; + +import { formatRowInfo } from '../utils.js'; +import WorkInfoRow from './WorkInfoRow.js'; + +interface Props { + className?: string; + value: CoreWorkplanType; + currentTimeSlice: number + isExpanded: boolean + region: RegionInfo | undefined +} + +function Workplan ({ currentTimeSlice, isExpanded, region, value: { core, info, lastBlock, type } }: Props): React.ReactElement { + const tableData: InfoRow = useMemo(() => { + return formatRowInfo(info, core, region, currentTimeSlice, type, lastBlock); + }, [info, core, region, currentTimeSlice, type, lastBlock]); + + if (!tableData) { + return ( + + + + + ); + } + + return ( + <> + {tableData && ( + + + + + )} + + + ); +} + +export default React.memo(Workplan); diff --git a/packages/page-broker/src/Overview/index.tsx b/packages/page-broker/src/Overview/index.tsx new file mode 100644 index 000000000000..cd46df2dd358 --- /dev/null +++ b/packages/page-broker/src/Overview/index.tsx @@ -0,0 +1,95 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { LinkOption } from '@polkadot/apps-config/endpoints/types'; +import type { CoreWorkload, CoreWorkplan, LegacyLease, Reservation } from '@polkadot/react-hooks/types'; +import type { PalletBrokerStatusRecord } from '@polkadot/types/lookup'; + +import React, { useEffect, useMemo, useState } from 'react'; + +import { useApi, useBrokerLeases, useBrokerReservations, useCall, useWorkloadInfos, useWorkplanInfos } from '@polkadot/react-hooks'; + +import { type CoreWorkloadType, type CoreWorkplanType } from '../types.js'; +import { createTaskMap, getOccupancyType } from '../utils.js'; +import CoresTable from './CoresTables.js'; +import Filters from './Filters.js'; +import Summary from './Summary.js'; + +interface Props { + className?: string; + apiEndpoint?: LinkOption | null; + api: ApiPromise; + isReady: boolean; +} + +const formatLoad = (data: CoreWorkplan[] | CoreWorkload[] | undefined, leaseMap: Record, reservationMap: Record): CoreWorkloadType[] | CoreWorkplanType[] => { + if (!data) { + return []; + } + + return data.map((w: CoreWorkload | CoreWorkplan) => { + const result = { + ...w, + lastBlock: leaseMap[w.info.task as number]?.until || 0, + type: getOccupancyType(leaseMap[w.info.task as number], reservationMap[w.info.task as number]) + }; + + if ('timeslice' in w) { + return result as CoreWorkplanType; + } else { + return result as CoreWorkloadType; + } + }); +}; + +function Overview ({ api, apiEndpoint, className, isReady }: Props): React.ReactElement { + const [workLoad, setWorkLoad] = useState(); + const [workPlan, setWorkPlan] = useState(); + const [filtered, setFiltered] = useState(); + const { isApiReady } = useApi(); + + const status = useCall(isReady && api.query.broker?.status); + const workloadInfos: CoreWorkload[] | undefined = useWorkloadInfos(api, isApiReady); + const workplanInfos: CoreWorkplan[] | undefined = useWorkplanInfos(api, isApiReady); + const reservations: Reservation[] | undefined = useBrokerReservations(api, isApiReady); + const leases: LegacyLease[] | undefined = useBrokerLeases(api, isApiReady); + + const leaseMap = useMemo(() => leases ? createTaskMap(leases) : [], [leases]); + const reservationMap = useMemo(() => reservations ? createTaskMap(reservations) : [], [reservations]); + + const timesliceAsString = useMemo(() => { + const timeslice = status?.toHuman().lastCommittedTimeslice?.toString(); + + return timeslice === undefined ? '' : timeslice.toString().split(',').join(''); + }, [status]); + + useEffect(() => + setWorkPlan(formatLoad(workplanInfos, leaseMap, reservationMap)) + , [workplanInfos, leaseMap, reservationMap]); + + useEffect(() => { + setWorkLoad(formatLoad(workloadInfos, leaseMap, reservationMap)); + }, [workloadInfos, leaseMap, reservationMap]); + + return ( +
+ + + +
+ ); +} + +export default React.memo(Overview); diff --git a/packages/page-broker/src/index.tsx b/packages/page-broker/src/index.tsx new file mode 100644 index 000000000000..7b0c5fbdf1b2 --- /dev/null +++ b/packages/page-broker/src/index.tsx @@ -0,0 +1,49 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { TabItem } from '@polkadot/react-components/types'; + +import React, { useRef } from 'react'; + +import { Tabs } from '@polkadot/react-components'; +import { useApi } from '@polkadot/react-hooks'; + +import Overview from './Overview/index.js'; +import { useTranslation } from './translate.js'; + +interface Props { + basePath: string; + className?: string; +} + +function createItemsRef (t: (key: string, options?: { replace: Record }) => string): TabItem[] { + return [ + { + isRoot: true, + name: 'overview', + text: t('Overview') + } + ]; +} + +function BrokerApp ({ basePath, className }: Props): React.ReactElement { + const { t } = useTranslation(); + const itemsRef = useRef(createItemsRef(t)); + const { api, apiEndpoint, isApiReady } = useApi(); + + return ( +
+ + +
+ ); +} + +export default React.memo(BrokerApp); diff --git a/packages/page-broker/src/translate.ts b/packages/page-broker/src/translate.ts new file mode 100644 index 000000000000..6de8c7786c53 --- /dev/null +++ b/packages/page-broker/src/translate.ts @@ -0,0 +1,8 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useTranslation as useTranslationBase } from 'react-i18next'; + +export function useTranslation (): { t: (key: string, options?: { replace: Record }) => string } { + return useTranslationBase('app-broker'); +} diff --git a/packages/page-broker/src/types.ts b/packages/page-broker/src/types.ts new file mode 100644 index 000000000000..2c9c88eac15d --- /dev/null +++ b/packages/page-broker/src/types.ts @@ -0,0 +1,45 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CoreWorkplan } from '@polkadot/react-hooks/types'; + +export interface InfoRow { + task: string | number, + maskBits: number, + core: number + mask?: string + start?: string | null, + end?: string | null + owner?: string + leaseLength?: number + endBlock?: number + type?: Occupancy +} + +export interface CoreInfo { + core: number, + workload: CoreWorkloadType[] | undefined, + workplan: CoreWorkplanType[] | undefined +} + +export interface statsType { + idles: number, + pools: number, + tasks: number +} + +export enum Occupancy { + 'Reservation', + 'Lease', + 'Rent' +} + +export interface CoreWorkplanType extends CoreWorkplan { + lastBlock: number, + type: Occupancy +} + +export interface CoreWorkloadType extends CoreWorkplan { + lastBlock: number, + type: Occupancy +} diff --git a/packages/page-broker/src/utils.ts b/packages/page-broker/src/utils.ts new file mode 100644 index 000000000000..31be9243e9d0 --- /dev/null +++ b/packages/page-broker/src/utils.ts @@ -0,0 +1,115 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CoreWorkloadInfo, LegacyLease, RegionInfo, Reservation } from '@polkadot/react-hooks/types'; +import type { CoreWorkloadType, InfoRow } from './types.js'; + +import { BN } from '@polkadot/util'; + +import { Occupancy } from './types.js'; + +const CoreTimeConsts = { + BlockTime: 6000, + BlocksPerTimeslice: 80, + DefaultRegion: 5040 +}; + +function formatDate (date: Date) { + const day = date.getDate(); + const month = date.toLocaleString('default', { month: 'short' }); + const year = date.getFullYear(); + + return `${day} ${month} ${year}`; +} + +export const estimateTime = (targetBlock: string, latestBlock: number, timestamp: number): string | null => { + if (!timestamp || !latestBlock || !targetBlock) { + console.error('Invalid input: one or more inputs are missing'); + + return null; + } + + try { + const blockTime = new BN(CoreTimeConsts.BlockTime); // Average block time in milliseconds (6 seconds) + const timeSlice = new BN(CoreTimeConsts.BlocksPerTimeslice); + const targetBlockBN = new BN(targetBlock).mul(timeSlice); + const latestBlockBN = new BN(latestBlock); + const timestampBN = new BN(timestamp); + const blockDifference = targetBlockBN.sub((latestBlockBN)).abs().mul(blockTime); + + const estTimestamp = targetBlockBN.lt(latestBlockBN) + ? timestampBN.sub(blockDifference) + : timestampBN.add(blockDifference); + + return formatDate(new Date(estTimestamp.toNumber())); + } catch (error) { + console.error('Error in calculation:', error); + + return null; + } +}; + +export function formatRowInfo (info: CoreWorkloadInfo, core: number, currentRegion: RegionInfo | undefined, timeslice: number, type: Occupancy, lastBlock: number, regionLength = CoreTimeConsts.DefaultRegion): InfoRow { + const item: InfoRow = { core, maskBits: info.maskBits, task: info.task }; + + if (currentRegion) { + const start = currentRegion?.start?.toString() ?? 0; + const end = currentRegion?.end?.toString() ?? 0; + const blockNumber = timeslice * 80; + + item.start = estimateTime(start, blockNumber, new Date().getTime()); + item.end = estimateTime(end, blockNumber, new Date().getTime()); + item.endBlock = Number(end) * 80; + item.owner = currentRegion?.owner.toString(); + } + + item.type = type; + + if (lastBlock) { + const blockNumber = timeslice * 80; + const period = Math.floor(lastBlock / regionLength); + const end = period * regionLength; + + item.start = ' - '; + item.end = estimateTime(end.toString(), blockNumber, new Date().getTime()); + item.endBlock = Number(end) * 80; + } + + return item; +} + +export function getStats (totalCores: string | undefined, workloadInfos: CoreWorkloadType[] | CoreWorkloadType | undefined) { + if (!totalCores || !workloadInfos) { + return { idles: 0, pools: 0, tasks: 0 }; + } + + const sanitized: CoreWorkloadType[] = Array.isArray(workloadInfos) ? workloadInfos : [workloadInfos]; + + const { pools, tasks } = sanitized.reduce( + (acc, { info }) => { + if (info.isTask) { + acc.tasks += 1; + } else if (info.isPool) { + acc.pools += 1; + } + + return acc; + }, + { pools: 0, tasks: 0 } + ); + const idles = Number(totalCores) - (pools + tasks); + + return { idles, pools, tasks }; +} + +export const createTaskMap = (items: T[]): Record => { + return (items || []).reduce((acc, item) => { + acc[Number(item.task)] = item; + + return acc; + }, {} as Record); +}; + +export const getOccupancyType = (lease: LegacyLease | undefined, reservation: Reservation | undefined): Occupancy => { + return reservation ? Occupancy.Reservation : lease ? Occupancy.Lease : Occupancy.Rent; +}; diff --git a/packages/page-broker/tsconfig.build.json b/packages/page-broker/tsconfig.build.json new file mode 100644 index 000000000000..0292e5e4c9fe --- /dev/null +++ b/packages/page-broker/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "..", + "outDir": "./build", + "rootDir": "./src" + }, + "references": [ + { "path": "../react-api/tsconfig.xref.json" }, + { "path": "../react-hooks/tsconfig.build.json" }, + { "path": "../react-components/tsconfig.build.json" } + ] +} diff --git a/packages/react-components/src/UsageBar.tsx b/packages/react-components/src/UsageBar.tsx new file mode 100644 index 000000000000..3ddaa385c1fe --- /dev/null +++ b/packages/react-components/src/UsageBar.tsx @@ -0,0 +1,107 @@ +// Copyright 2017-2024 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { styled } from '@polkadot/react-components'; + +interface PieChartProps { + data: { label: string; value: number; color: string }[]; +} + +const Container = styled.div` + display: flex; + align-items: center; +`; + +const GraphContainer = styled.div` + position: relative; +`; + +const LegendContainer = styled.div` + display: flex; + flex-direction: column; + margin-left: 20px; +`; + +const LegendItem = styled.div` + display: flex; + align-items: center; + margin-bottom: 10px; +`; + +const ColorBox = styled.div<{ color: string }>` + width: 20px; + height: 20px; + background-color: ${(props: { color: string }) => props.color}; + margin-right: 10px; +`; + +function UsageBar ({ data }: PieChartProps): React.ReactElement { + const radius = 50; + const strokeWidth = 15; + const circumference = 2 * Math.PI * radius; + + const total = data.reduce((acc, item) => acc + item.value, 0); + + let cumulativeOffset = 0; + + if (!total) { + return <>; + } + + return ( + + + + + {data.map((item, index) => { + const percentage = (item.value / total) * 100; + const dashArray = (percentage / 100) * circumference; + const dashOffset = (cumulativeOffset / 100) * circumference; + + cumulativeOffset += percentage; + + return ( + + {`${item.label}: ${percentage.toFixed(2)}%`} + + ); + })} + + + + {data.map((item, index) => ( + + + {`${item.label}: ${((item.value / total) * 100).toFixed(2)}%`} + + ))} + + + ); +} + +export default React.memo(UsageBar); diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 5d4599b09bc8..4699f6057f3c 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -96,6 +96,7 @@ export { default as Toggle } from './Toggle.js'; export { default as ToggleGroup } from './ToggleGroup.js'; export { default as Tooltip } from './Tooltip.js'; export { default as TxButton } from './TxButton.js'; +export { default as UsageBar } from './UsageBar.js'; export { default as VoteAccount } from './VoteAccount.js'; export { default as VoteValue } from './VoteValue.js'; diff --git a/packages/react-components/src/styles/index.ts b/packages/react-components/src/styles/index.ts index 56b6fe4e3e7e..162444a4522a 100644 --- a/packages/react-components/src/styles/index.ts +++ b/packages/react-components/src/styles/index.ts @@ -18,7 +18,7 @@ const FACTORS = [0.2126, 0.7152, 0.0722]; const PARTS = [0, 2, 4]; const VERY_DARK = 16; -const defaultHighlight = '#f19135'; +export const defaultHighlight = '#f19135'; function getHighlight (uiHighlight: string | undefined): string { return (uiHighlight || defaultHighlight); diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 7df5a8db700b..64a71863b8d8 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -22,11 +22,16 @@ export { useBlockEvents } from './useBlockEvents.js'; export { useBlockInterval } from './useBlockInterval.js'; export { useBlocksPerDays } from './useBlocksPerDays.js'; export { useBlockTime } from './useBlockTime.js'; +export { useBrokerLeases } from './useBrokerLeases.js'; +export { useBrokerReservations } from './useBrokerReservations.js'; +export { useBrokerStatus } from './useBrokerStatus.js'; export { useCacheKey } from './useCacheKey.js'; export { useCall } from './useCall.js'; export { useCallMulti } from './useCallMulti.js'; export { useCollectiveInstance } from './useCollectiveInstance.js'; export { useCollectiveMembers } from './useCollectiveMembers.js'; +export { useCoreDescriptor } from './useCoreDescriptor.js'; +export { useCurrentPrice } from './useCurrentPrice.js'; export { useDebounce } from './useDebounce.js'; export { useDelegations } from './useDelegations.js'; export { useDeriveAccountFlags } from './useDeriveAccountFlags.js'; @@ -66,7 +71,10 @@ export { usePopupWindow } from './usePopupWindow.js'; export { usePreimage } from './usePreimage.js'; export { useProxies } from './useProxies.js'; export { useQueue } from './useQueue.js'; +export { useQueueStatus } from './useQueueStatus.js'; +export { useRegions } from './useRegions.js'; export { useRegistrars } from './useRegistrars.js'; +export { useRenewalBump } from './useRenewalBump.js'; export { useSavedFlags } from './useSavedFlags.js'; export { useScroll } from './useScroll.js'; export { useStakingInfo } from './useStakingInfo.js'; @@ -84,3 +92,5 @@ export { useVotingStatus } from './useVotingStatus.js'; export { useWeight } from './useWeight.js'; export { useWindowColumns } from './useWindowColumns.js'; export { useWindowSize } from './useWindowSize.js'; +export { useWorkloadInfos } from './useWorkloadInfos.js'; +export { useWorkplanInfos } from './useWorkplanInfos.js'; diff --git a/packages/react-hooks/src/types.ts b/packages/react-hooks/src/types.ts index 227ee500de06..04e0b437ebac 100644 --- a/packages/react-hooks/src/types.ts +++ b/packages/react-hooks/src/types.ts @@ -8,7 +8,7 @@ import type { DeriveAccountFlags, DeriveAccountRegistration } from '@polkadot/ap import type { DisplayedJudgement } from '@polkadot/react-components/types'; import type { Option, u32, u128, Vec } from '@polkadot/types'; import type { AccountId, BlockNumber, Call, Hash, SessionIndex, ValidatorPrefs } from '@polkadot/types/interfaces'; -import type { PalletPreimageRequestStatus, PalletStakingRewardDestination, PalletStakingStakingLedger, SpStakingExposurePage, SpStakingPagedExposureMetadata } from '@polkadot/types/lookup'; +import type { PalletPreimageRequestStatus, PalletStakingRewardDestination, PalletStakingStakingLedger, PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor, SpStakingExposurePage, SpStakingPagedExposureMetadata } from '@polkadot/types/lookup'; import type { ICompact, IExtrinsic, INumber } from '@polkadot/types/types'; import type { KeyringJson$Meta } from '@polkadot/ui-keyring/types'; import type { BN } from '@polkadot/util'; @@ -19,7 +19,7 @@ export type CallParam = any; export type CallParams = [] | CallParam[]; -export interface CallOptions { +export interface CallOptions { defaultValue?: T; // eslint-disable-next-line @typescript-eslint/no-explicit-any paramMap?: (params: any) => CallParams; @@ -214,3 +214,61 @@ export interface WeightResult { v1Weight: BN; v2Weight: V2WeightConstruct; } + +export interface CoreDescription { + core: number; + info: PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor[]; +} + +export interface OnDemandQueueStatus { + traffic: u128; + nextIndex: u32; + smallestIndex: u32; + freedIndices: [string, u32][]; +} + +export interface CoreWorkload { + core: number, + info: CoreWorkloadInfo +} + +export interface CoreWorkloadInfo { + task: number | string, + isTask: boolean + isPool: boolean + mask: string[] + maskBits: number +} +export interface CoreWorkplan { + core: number; + info: CoreWorkplanInfo + timeslice: number; +} + +export interface CoreWorkplanInfo { + task: number | string, + isTask: boolean + isPool: boolean + mask: string[] + maskBits: number +} + +export interface RegionInfo { + core: number, + start: number, + end: number, + owner: string, + paid: string, + mask: `0x${string}` +} + +export interface Reservation { + task: string + mask: number, +} + +export interface LegacyLease { + core: number, + until: number, + task: string +} diff --git a/packages/react-hooks/src/useBrokerLeases.ts b/packages/react-hooks/src/useBrokerLeases.ts new file mode 100644 index 000000000000..441bbd066f1d --- /dev/null +++ b/packages/react-hooks/src/useBrokerLeases.ts @@ -0,0 +1,34 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { Vec } from '@polkadot/types'; +import type { PalletBrokerLeaseRecordItem } from '@polkadot/types/lookup'; +import type { LegacyLease } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useCall } from '@polkadot/react-hooks'; + +function useBrokerLeasesImpl (api: ApiPromise, ready: boolean): LegacyLease[] | undefined { + const leases = useCall>(ready && api.query.broker.leases); + const [state, setState] = useState(); + + useEffect((): void => { + if (!leases) { + return; + } + + setState( + leases.map((info, index: number) => ({ + core: index, + task: info.task.toString(), + until: info.until.toNumber() + }) + )); + }, [leases]); + + return state; +} + +export const useBrokerLeases = createNamedHook('useBrokerLeases', useBrokerLeasesImpl); diff --git a/packages/react-hooks/src/useBrokerReservations.ts b/packages/react-hooks/src/useBrokerReservations.ts new file mode 100644 index 000000000000..4c9afe664645 --- /dev/null +++ b/packages/react-hooks/src/useBrokerReservations.ts @@ -0,0 +1,37 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { Vec } from '@polkadot/types'; +import type { PalletBrokerScheduleItem } from '@polkadot/types/lookup'; +import type { Reservation } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useCall } from '@polkadot/react-hooks'; + +import { processHexMask } from './utils/dataProcessing.js'; + +function useBrokerReservationsImpl (api: ApiPromise, ready: boolean): Reservation[] | undefined { + const reservations = useCall<[any, Vec>[]]>(ready && api.query.broker.reservations); + const [state, setState] = useState(); + + useEffect((): void => { + if (!reservations) { + return; + } + + setState( + reservations.map((info: PalletBrokerScheduleItem[]) => { + return { + mask: processHexMask(info[0]?.mask)?.length ?? 0, + task: info[0]?.assignment?.isTask ? info[0]?.assignment?.asTask.toString() : info[0]?.assignment?.isPool ? 'Pool' : '' + }; + } + )); + }, [reservations]); + + return state; +} + +export const useBrokerReservations = createNamedHook('useBrokerReservations', useBrokerReservationsImpl); diff --git a/packages/react-hooks/src/useBrokerStatus.ts b/packages/react-hooks/src/useBrokerStatus.ts new file mode 100644 index 000000000000..87a39a229f82 --- /dev/null +++ b/packages/react-hooks/src/useBrokerStatus.ts @@ -0,0 +1,25 @@ +// Copyright 2017-2024 @polkadot/react-query authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PalletBrokerStatusRecord } from '@polkadot/types/lookup'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; + +function useBrokerStatusImpl (query: string): string | undefined { + const { api } = useApi(); + const status = useCall(api.query.broker?.status); + const [state, setState] = useState(); + + useEffect((): void => { + status && + setState( + status + ); + }, [status]); + + return state?.toJSON()[query]?.toString(); +} + +export const useBrokerStatus = createNamedHook('useBrokerStatus', useBrokerStatusImpl); diff --git a/packages/react-hooks/src/useCoreDescriptor.ts b/packages/react-hooks/src/useCoreDescriptor.ts new file mode 100644 index 000000000000..c7eca803969f --- /dev/null +++ b/packages/react-hooks/src/useCoreDescriptor.ts @@ -0,0 +1,51 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { StorageKey, u32, Vec } from '@polkadot/types'; +import type { PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor } from '@polkadot/types/lookup'; +import type { CoreDescription } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useCall, useMapKeys } from '@polkadot/react-hooks'; + +function extractInfo (info: PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor[], core: number) { + return { + core, + info + }; +} + +const OPT_KEY = { + transform: (keys: StorageKey<[u32]>[]): u32[] => + keys.map(({ args: [id] }) => id) +}; + +function useCoreDescriptorImpl (api: ApiPromise, ready: boolean): CoreDescription[] | undefined { + const keys = useMapKeys(ready && api.query.coretimeAssignmentProvider.coreDescriptors, [], OPT_KEY); + + const sanitizedKeys = keys?.map((_, index) => { + return index; + }); + + sanitizedKeys?.pop(); + + const coreDescriptors = useCall<[[number[]], Vec[]]>(ready && api.query.coretimeAssignmentProvider.coreDescriptors.multi, [sanitizedKeys], { withParams: true }); + + const [state, setState] = useState(); + + useEffect((): void => { + coreDescriptors && + setState( + coreDescriptors[0][0].map((info, index) => { + return extractInfo(coreDescriptors[1][index], info); + } + ) + ); + }, [coreDescriptors]); + + return state; +} + +export const useCoreDescriptor = createNamedHook('useCoreDescriptor', useCoreDescriptorImpl); diff --git a/packages/react-hooks/src/useCurrentPrice.tsx b/packages/react-hooks/src/useCurrentPrice.tsx new file mode 100644 index 000000000000..6fc7c028721f --- /dev/null +++ b/packages/react-hooks/src/useCurrentPrice.tsx @@ -0,0 +1,31 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PalletBrokerSaleInfoRecord } from '@polkadot/types/lookup'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; + +function extractCurrentPrice (saleInfo: PalletBrokerSaleInfoRecord) { + return saleInfo.toJSON().price?.toString(); +} + +function useCurrentPriceImpl () { + const { api, isApiReady } = useApi(); + + const saleInfo = useCall(isApiReady && api.query.broker.saleInfo); + + const [state, setState] = useState(); + + useEffect((): void => { + saleInfo && + setState( + extractCurrentPrice(saleInfo) + ); + }, [saleInfo]); + + return state; +} + +export const useCurrentPrice = createNamedHook('useCurrentPrice', useCurrentPriceImpl); diff --git a/packages/react-hooks/src/useQueueStatus.ts b/packages/react-hooks/src/useQueueStatus.ts new file mode 100644 index 000000000000..fa4271f6b63c --- /dev/null +++ b/packages/react-hooks/src/useQueueStatus.ts @@ -0,0 +1,36 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { OnDemandQueueStatus } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; + +function extractInfo (value: OnDemandQueueStatus) { + return { + freedIndices: value.freedIndices, + nextIndex: value.nextIndex, + smallestIndex: value.smallestIndex, + traffic: value.traffic + }; +} + +function useQueueStatusImpl (): OnDemandQueueStatus | undefined { + const { api } = useApi(); + + const queue = useCall(api.query.onDemandAssignmentProvider.queueStatus); + + const [state, setState] = useState(); + + useEffect((): void => { + queue && + setState( + extractInfo(queue) + ); + }, [queue]); + + return state; +} + +export const useQueueStatus = createNamedHook('useQueueStatus', useQueueStatusImpl); diff --git a/packages/react-hooks/src/useRegions.ts b/packages/react-hooks/src/useRegions.ts new file mode 100644 index 000000000000..4570c3c39c91 --- /dev/null +++ b/packages/react-hooks/src/useRegions.ts @@ -0,0 +1,49 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { Option, StorageKey } from '@polkadot/types'; +import type { PalletBrokerRegionId, PalletBrokerRegionRecord } from '@polkadot/types/lookup'; +import type { RegionInfo } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useCall, useMapKeys } from '@polkadot/react-hooks'; + +function extractInfo (core: number, start: number, end: number, owner: string, paid: string, mask: `0x${string}`) { + return { + core, + end, + mask, + owner, + paid, + start + }; +} + +const OPT_KEY = { + transform: (keys: StorageKey<[PalletBrokerRegionId]>[]): PalletBrokerRegionId[] => + keys.map(({ args: [regionId] }) => regionId) +}; + +function useRegionsImpl (api: ApiPromise): RegionInfo[] | undefined { + const regionKeys = useMapKeys(api.query.broker.regions, [], OPT_KEY); + + const regionInfo = useCall<[[PalletBrokerRegionId[]], Option[]]>(api.query.broker.regions.multi, [regionKeys], { withParams: true }); + + const [state, setState] = useState(); + + useEffect((): void => { + regionInfo && + regionInfo[0][0].length > 0 && + setState( + regionInfo[0][0].map((info, index) => + extractInfo(info.core.toNumber(), info.begin.toNumber(), regionInfo[1][index].unwrap().end.toNumber(), regionInfo[1][index].unwrap().owner.toString(), regionInfo[1][index].unwrap().paid.toString(), info.mask.toHex()) + ) + ); + }, [regionInfo]); + + return state; +} + +export const useRegions = createNamedHook('useRegions', useRegionsImpl); diff --git a/packages/react-hooks/src/useRenewalBump.ts b/packages/react-hooks/src/useRenewalBump.ts new file mode 100644 index 000000000000..9ae0fc1374a8 --- /dev/null +++ b/packages/react-hooks/src/useRenewalBump.ts @@ -0,0 +1,31 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PalletBrokerConfigRecord } from '@polkadot/types/lookup'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; + +function extractRenewalBump (config: PalletBrokerConfigRecord) { + return config.toJSON().renewalBump?.toString(); +} + +function useRenewalBumpImpl () { + const { api, isApiReady } = useApi(); + + const config = useCall(isApiReady && api.query.broker.configuration); + + const [state, setState] = useState(); + + useEffect((): void => { + config && + setState( + extractRenewalBump(config) + ); + }, [config]); + + return state; +} + +export const useRenewalBump = createNamedHook('useRenewalBump', useRenewalBumpImpl); diff --git a/packages/react-hooks/src/useWorkloadInfos.ts b/packages/react-hooks/src/useWorkloadInfos.ts new file mode 100644 index 000000000000..32dd71389e32 --- /dev/null +++ b/packages/react-hooks/src/useWorkloadInfos.ts @@ -0,0 +1,67 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { StorageKey, u32, Vec } from '@polkadot/types'; +import type { PalletBrokerScheduleItem } from '@polkadot/types/lookup'; +import type { BN } from '@polkadot/util'; +import type { CoreWorkload } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useCall, useMapKeys } from '@polkadot/react-hooks'; + +import { processHexMask } from './utils/dataProcessing.js'; + +export function sortByCore (dataArray?: T | T[]): T[] { + if (!dataArray) { + return []; + } + + const sanitized = Array.isArray(dataArray) ? dataArray : [dataArray]; + + return sanitized.sort((a, b) => a.core - b.core); +} + +function extractInfo (info: PalletBrokerScheduleItem[], core: number): CoreWorkload { + const mask: string[] = processHexMask(info[0]?.mask); + const assignment = info[0].assignment; + + return { + core, + info: { + isPool: assignment.isPool, + isTask: assignment.isTask, + mask, + maskBits: mask.length, + task: assignment.isTask ? assignment.asTask.toString() : assignment.isPool ? 'Pool' : '' + } + }; +} + +const OPT_KEY = { + transform: (keys: StorageKey<[u32]>[]): u32[] => + keys.map(({ args: [core] }) => core) +}; + +function useWorkloadInfosImpl (api: ApiPromise, ready: boolean): CoreWorkload[] | undefined { + const cores = useMapKeys(ready && api.query.broker.workload, [], OPT_KEY); + const workloadInfo = useCall<[[BN[]], Vec[]]>(ready && api.query.broker.workload.multi, [cores], { withParams: true }); + const [state, setState] = useState(); + + useEffect((): void => { + if (!workloadInfo?.[0]?.[0]) { + return; + } + + const cores = workloadInfo[0][0]; + + setState( + sortByCore(cores.map((core, index) => extractInfo(workloadInfo[1][index], core.toNumber()))) + ); + }, [workloadInfo]); + + return state; +} + +export const useWorkloadInfos = createNamedHook('useWorkloadInfos', useWorkloadInfosImpl); diff --git a/packages/react-hooks/src/useWorkplanInfos.ts b/packages/react-hooks/src/useWorkplanInfos.ts new file mode 100644 index 000000000000..13bd487facb6 --- /dev/null +++ b/packages/react-hooks/src/useWorkplanInfos.ts @@ -0,0 +1,76 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { Option, StorageKey, u16, u32, Vec } from '@polkadot/types'; +import type { PalletBrokerScheduleItem } from '@polkadot/types/lookup'; +import type { BN } from '@polkadot/util'; +import type { CoreWorkplan } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useCall, useMapKeys } from '@polkadot/react-hooks'; + +import { processHexMask } from './utils/dataProcessing.js'; + +export function sortByCore (dataArray?: T | T[]): T[] { + if (!dataArray) { + return []; + } + + const sanitized = Array.isArray(dataArray) ? dataArray : [dataArray]; + + return sanitized.sort((a, b) => a.core - b.core); +} + +function extractInfo (info: Vec, core: number, timeslice: number): CoreWorkplan { + const mask: string[] = processHexMask(info[0]?.mask); + const assignment = info[0].assignment; + + return { + core, + info: { + isPool: assignment.isPool, + isTask: assignment.isTask, + mask, + maskBits: mask.length, + task: assignment.isTask ? assignment.asTask.toString() : assignment.isPool ? 'Pool' : '' + }, + timeslice + }; +} + +const OPT_KEY = { + transform: (keys: StorageKey<[u32, u16]>[]): [u32, u16][] => + keys.map(({ args: [timeslice, core] }) => [timeslice, core]) +}; + +function useWorkplanInfosImpl (api: ApiPromise, ready: boolean): CoreWorkplan[] | undefined { + const workplanKeys = useMapKeys(ready && api.query.broker.workplan, [], OPT_KEY); + + const sanitizedKeys = workplanKeys?.map((value) => { + return value[0]; + }); + + const workplanInfo = useCall<[[[u32, u16][]], Option>[]]>(ready && api.query.broker.workplan.multi, [sanitizedKeys], { withParams: true }); + + const [state, setState] = useState(); + + useEffect(() => { + if (!workplanInfo?.[1] || !workplanInfo[0]?.[0]) { + return; + } + + const coreInfo = workplanInfo[0][0]; + + setState( + sortByCore(coreInfo.map((core: BN[], index) => + extractInfo(workplanInfo[1][index].unwrap(), core[1].toNumber(), core[0].toNumber()) + )) + ); + }, [workplanInfo]); + + return state; +} + +export const useWorkplanInfos = createNamedHook('useWorkplanInfos', useWorkplanInfosImpl); diff --git a/packages/react-hooks/src/utils/dataProcessing.ts b/packages/react-hooks/src/utils/dataProcessing.ts new file mode 100644 index 000000000000..3c788a43e086 --- /dev/null +++ b/packages/react-hooks/src/utils/dataProcessing.ts @@ -0,0 +1,25 @@ +// Copyright 2017-2024 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PalletBrokerScheduleItem } from '@polkadot/types/lookup'; + +export function hexToBin (hex: string): string { + return parseInt(hex, 16).toString(2); +} + +export function processHexMask (mask: PalletBrokerScheduleItem['mask'] | undefined): string[] { + if (!mask) { + return []; + } + + const trimmedHex: string = mask.toHex().slice(2); + const arr: string[] = trimmedHex.split(''); + const buffArr: string[] = []; + + arr.forEach((bit) => { + hexToBin(bit).split('').forEach((v) => buffArr.push(v)); + }); + buffArr.filter((v) => v === '1'); + + return buffArr; +} diff --git a/packages/react-query/src/BrokerStatus.tsx b/packages/react-query/src/BrokerStatus.tsx new file mode 100644 index 000000000000..d0f5a712b188 --- /dev/null +++ b/packages/react-query/src/BrokerStatus.tsx @@ -0,0 +1,25 @@ +// Copyright 2017-2024 @polkadot/react-query authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { useBrokerStatus } from '@polkadot/react-hooks'; + +interface Props { + children?: React.ReactNode; + className?: string; + query: string; +} + +function BrokerStatus ({ children, className = '', query }: Props): React.ReactElement { + const info = useBrokerStatus(query); + + return ( +
+ {info} + {children} +
+ ); +} + +export default React.memo(BrokerStatus); diff --git a/packages/react-query/src/CoreDescriptor.tsx b/packages/react-query/src/CoreDescriptor.tsx new file mode 100644 index 000000000000..ba4e6bb4e7c3 --- /dev/null +++ b/packages/react-query/src/CoreDescriptor.tsx @@ -0,0 +1,29 @@ +// Copyright 2017-2024 @polkadot/react-query authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor } from '@polkadot/types/lookup'; + +import React from 'react'; + +import { useApi, useCall } from '@polkadot/react-hooks'; + +interface Props { + children?: React.ReactNode; + className?: string; + query: string; +} + +function BrokerStatus ({ children, className = '', query }: Props): React.ReactElement { + const { api } = useApi(); + const status = useCall(api.query.broker?.status); + const strStatus = status === undefined ? '' : status.toJSON()[query]; + + return ( +
+ {strStatus?.toString()} + {children} +
+ ); +} + +export default React.memo(BrokerStatus); diff --git a/packages/react-query/src/FormatBalance.tsx b/packages/react-query/src/FormatBalance.tsx index 1493b61453ef..f4bd486717ff 100644 --- a/packages/react-query/src/FormatBalance.tsx +++ b/packages/react-query/src/FormatBalance.tsx @@ -21,6 +21,7 @@ interface Props { isShort?: boolean; label?: React.ReactNode; labelPost?: LabelPost; + useTicker?: boolean; value?: Compact | BN | string | number | null; valueFormatted?: string; withCurrency?: boolean; @@ -47,8 +48,12 @@ function getFormat (registry: Registry, formatIndex = 0): [number, string] { ]; } -function createElement (prefix: string, postfix: string, unit: string, label: LabelPost = '', isShort = false): React.ReactNode { - return <>{`${prefix}${isShort ? '' : '.'}`}{!isShort && {`0000${postfix || ''}`.slice(-4)}} {unit}{label}; +function createElement (prefix: string, postfix: string, unit: string, label: LabelPost = '', isShort = false, ticker?: string): React.ReactNode { + if (ticker) { + return <>{`${prefix}${isShort ? '' : '.'}`}{!isShort && {`${postfix || ''}`.slice(-4)}} {ticker}{label}; + } else { + return <>{`${prefix}${isShort ? '' : '.'}`}{!isShort && {`0000${postfix || ''}`.slice(-4)}} {unit}{label}; + } } function splitFormat (value: string, label?: LabelPost, isShort?: boolean): React.ReactNode { @@ -58,8 +63,8 @@ function splitFormat (value: string, label?: LabelPost, isShort?: boolean): Reac return createElement(prefix, postfix, unit, label, isShort); } -function applyFormat (value: Compact | BN | string | number, [decimals, token]: [number, string], withCurrency = true, withSi?: boolean, _isShort?: boolean, labelPost?: LabelPost): React.ReactNode { - const [prefix, postfix] = formatBalance(value, { decimals, forceUnit: '-', withSi: false }).split('.'); +function applyFormat (value: Compact | BN | string | number, [decimals, token]: [number, string], withCurrency = true, withSi?: boolean, _isShort?: boolean, labelPost?: LabelPost, useTicker?: boolean): React.ReactNode { + const [prefix, postfix, ticker] = formatBalance(value, { decimals }).split(/[.\s]+/); const isShort = _isShort || (withSi && prefix.length >= K_LENGTH); const unitPost = withCurrency ? token : ''; @@ -71,10 +76,14 @@ function applyFormat (value: Compact | BN | string | number, [decimals, tok return <>{major}.{minor}{unit}{unit ? unitPost : ` ${unitPost}`}{labelPost || ''}; } - return createElement(prefix, postfix, unitPost, labelPost, isShort); + if (useTicker) { + return createElement(prefix, postfix, unitPost, labelPost, isShort, ticker); + } else { + return createElement(prefix, postfix, unitPost, labelPost, isShort); + } } -function FormatBalance ({ children, className = '', format, formatIndex, isShort, label, labelPost, value, valueFormatted, withCurrency, withSi }: Props): React.ReactElement { +function FormatBalance ({ children, className = '', format, formatIndex, isShort, label, labelPost, useTicker, value, valueFormatted, withCurrency, withSi }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); @@ -96,7 +105,7 @@ function FormatBalance ({ children, className = '', format, formatIndex, isShort : value ? value === 'all' ? <>{t('everything')}{labelPost || ''} - : applyFormat(value, formatInfo, withCurrency, withSi, isShort, labelPost) + : applyFormat(value, formatInfo, withCurrency, withSi, isShort, labelPost, useTicker) : isString(labelPost) ? `-${labelPost.toString()}` : labelPost @@ -122,7 +131,6 @@ const StyledSpan = styled.span` .ui--FormatBalance-unit { font-size: var(--font-percent-tiny); - text-transform: uppercase; } .ui--FormatBalance-value { diff --git a/packages/react-query/src/PoolSize.tsx b/packages/react-query/src/PoolSize.tsx new file mode 100644 index 000000000000..04cd041d50e5 --- /dev/null +++ b/packages/react-query/src/PoolSize.tsx @@ -0,0 +1,38 @@ +// Copyright 2017-2024 @polkadot/react-query authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PalletBrokerStatusRecord } from '@polkadot/types/lookup'; + +import React from 'react'; + +import { useApi, useCall } from '@polkadot/react-hooks'; + +interface Props { + children?: React.ReactNode; + className?: string; +} + +function PoolSize ({ children, className = '' }: Props): React.ReactElement { + const { api } = useApi(); + const status = useCall(api.query.broker?.status); + let systemPool = 0; + let privatePool = 0; + let poolSize = ''; + + if (status === undefined) { + poolSize = '0'; + } else { + systemPool = status.toJSON().systemPoolSize as number; + privatePool = status.toJSON().systemPoolSize as number; + poolSize = (systemPool + privatePool).toString(); + } + + return ( +
+ {poolSize} + {children} +
+ ); +} + +export default React.memo(PoolSize); diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts index 6d92730788e1..53d94bf4566b 100644 --- a/packages/react-query/src/index.ts +++ b/packages/react-query/src/index.ts @@ -8,6 +8,7 @@ export { default as BestFinalized } from './BestFinalized.js'; export { default as BestNumber } from './BestNumber.js'; export { default as BlockToTime } from './BlockToTime.js'; export { default as Bonded } from './Bonded.js'; +export { default as BrokerStatus } from './BrokerStatus.js'; export { default as Chain } from './Chain.js'; export { default as Elapsed } from './Elapsed.js'; export { default as FormatBalance } from './FormatBalance.js'; @@ -15,6 +16,7 @@ export { default as LockedVote } from './LockedVote.js'; export { default as NodeName } from './NodeName.js'; export { default as NodeVersion } from './NodeVersion.js'; export { default as Nonce } from './Nonce.js'; +export { default as PoolSize } from './PoolSize.js'; export { default as SessionToTime } from './SessionToTime.js'; export { default as TimeNow } from './TimeNow.js'; export { default as TotalInactive } from './TotalInactive.js'; diff --git a/tsconfig.base.json b/tsconfig.base.json index 18371ad57522..662448d98d4f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -25,6 +25,7 @@ "@polkadot/app-alliance": ["page-alliance/src/index.tsx"], "@polkadot/app-ambassador": ["page-ambassador/src/index.tsx"], "@polkadot/app-assets": ["page-assets/src/index.tsx"], + "@polkadot/app-broker": ["page-broker/src/index.tsx"], "@polkadot/app-bounties": ["page-bounties/src/index.tsx"], "@polkadot/app-calendar": ["page-calendar/src/index.tsx"], "@polkadot/app-claims": ["page-claims/src/index.tsx"], diff --git a/tsconfig.build.json b/tsconfig.build.json index a8951e88c66b..bea32db31494 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -21,6 +21,7 @@ { "path": "./packages/page-bounties/tsconfig.build.json" }, { "path": "./packages/page-bounties/tsconfig.spec.json" }, { "path": "./packages/page-bounties/tsconfig.test.json" }, + { "path": "./packages/page-broker/tsconfig.build.json" }, { "path": "./packages/page-calendar/tsconfig.build.json" }, { "path": "./packages/page-claims/tsconfig.build.json" }, { "path": "./packages/page-claims/tsconfig.spec.json" }, diff --git a/yarn.lock b/yarn.lock index 013fb5f17d46..3ded225252be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1343,6 +1343,19 @@ __metadata: languageName: unknown linkType: soft +"@polkadot/app-broker@workspace:packages/page-broker": + version: 0.0.0-use.local + resolution: "@polkadot/app-broker@workspace:packages/page-broker" + dependencies: + "@polkadot/react-components": "npm:0.143.3-21-x" + "@polkadot/react-query": "npm:0.143.3-21-x" + peerDependencies: + react: "*" + react-dom: "*" + react-is: "*" + languageName: unknown + linkType: soft + "@polkadot/app-calendar@workspace:packages/page-calendar": version: 0.0.0-use.local resolution: "@polkadot/app-calendar@workspace:packages/page-calendar" @@ -2122,7 +2135,7 @@ __metadata: languageName: unknown linkType: soft -"@polkadot/react-components@npm:^0.143.3-21-x, @polkadot/react-components@workspace:packages/react-components": +"@polkadot/react-components@npm:0.143.3-21-x, @polkadot/react-components@npm:^0.143.3-21-x, @polkadot/react-components@workspace:packages/react-components": version: 0.0.0-use.local resolution: "@polkadot/react-components@workspace:packages/react-components" dependencies: @@ -2246,7 +2259,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/react-query@npm:^0.143.3-21-x, @polkadot/react-query@workspace:packages/react-query": +"@polkadot/react-query@npm:0.143.3-21-x, @polkadot/react-query@npm:^0.143.3-21-x, @polkadot/react-query@workspace:packages/react-query": version: 0.0.0-use.local resolution: "@polkadot/react-query@workspace:packages/react-query" peerDependencies: @@ -2905,7 +2918,14 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5, @scure/base@npm:^1.1.7, @scure/base@npm:~1.1.0": +"@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5, @scure/base@npm:~1.1.0": + version: 1.1.5 + resolution: "@scure/base@npm:1.1.5" + checksum: 10/543fa9991c6378b6a0d5ab7f1e27b30bb9c1e860d3ac81119b4213cfdf0ad7b61be004e06506e89de7ce0cec9391c17f5c082bb34c3b617a2ee6a04129f52481 + languageName: node + linkType: hard + +"@scure/base@npm:^1.1.7": version: 1.1.7 resolution: "@scure/base@npm:1.1.7" checksum: 10/fc50ffaab36cb46ff9fa4dc5052a06089ab6a6707f63d596bb34aaaec76173c9a564ac312a0b981b5e7a5349d60097b8878673c75d6cbfc4da7012b63a82099b @@ -3304,7 +3324,14 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": +"@types/estree@npm:*, @types/estree@npm:^1.0.0": + version: 1.0.0 + resolution: "@types/estree@npm:1.0.0" + checksum: 10/9ec366ea3b94db26a45262d7161456c9ee25fd04f3a0da482f6e97dbf90c0c8603053c311391a877027cc4ee648340f988cd04f11287886cdf8bc23366291ef9 + languageName: node + linkType: hard + +"@types/estree@npm:1.0.5": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" checksum: 10/7de6d928dd4010b0e20c6919e1a6c27b61f8d4567befa89252055fad503d587ecb9a1e3eab1b1901f923964d7019796db810b7fd6430acb26c32866d126fd408 @@ -10208,11 +10235,11 @@ __metadata: linkType: hard "i18next@npm:*, i18next@npm:^23.7.11": - version: 23.12.2 - resolution: "i18next@npm:23.12.2" + version: 23.7.11 + resolution: "i18next@npm:23.7.11" dependencies: "@babel/runtime": "npm:^7.23.2" - checksum: 10/d7a743c54b83acc1203315e547bfe830bfe825dddd7706646aec2a49cb74254bcda70645b568d1bed55ee3610ba5e6f6012fb3c13f03080c1dd0f99db2c45478 + checksum: 10/1127bc17f94459d40bd9aaa0350e9786d3853eb82449aabb4514e187fafc752c76a3f52c6be1c2722bfdadaa74f0d26b4f7dd04528ba6b2de7e34f5c6c019c21 languageName: node linkType: hard