diff --git a/packages/apps-config/src/endpoints/productionRelayKusama.ts b/packages/apps-config/src/endpoints/productionRelayKusama.ts index 30a00ef29af7..412d93e49b97 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 d5ac329bd8b4..8ba298b98907 100644 --- a/packages/apps-config/src/endpoints/testingRelayRococo.ts +++ b/packages/apps-config/src/endpoints/testingRelayRococo.ts @@ -732,7 +732,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 908b7bf627ea..8fcd12fcae02 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..f2922ec0fa2e --- /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: 'calendar-clock', + name: 'broker', + text: t('nav.broker', 'Coretime Broker (Experimental)', { ns: 'app-broker' }) + }; +} diff --git a/packages/apps-routing/src/coretime.ts b/packages/apps-routing/src/coretime.ts new file mode 100644 index 000000000000..89637a2ed7df --- /dev/null +++ b/packages/apps-routing/src/coretime.ts @@ -0,0 +1,23 @@ +// 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-coretime'; + +export default function create (t: TFunction): Route { + return { + Component, + display: { + needsApi: [ + + 'query.coretimeAssignmentProvider.coreDescriptors' + ], + needsApiInstances: true + }, + group: 'network', + icon: 'calendar-clock', + name: 'coretime', + text: t('nav.coretime', 'Coretime (Experimental)', { ns: 'app-coretime' }) + }; +} diff --git a/packages/apps-routing/src/index.ts b/packages/apps-routing/src/index.ts index 32ee324a5442..952654fdde3e 100644 --- a/packages/apps-routing/src/index.ts +++ b/packages/apps-routing/src/index.ts @@ -9,10 +9,12 @@ 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'; import contracts from './contracts.js'; +import coretime from './coretime.js'; import council from './council.js'; import democracy from './democracy.js'; import explorer from './explorer.js'; @@ -61,6 +63,8 @@ export default function create (t: TFunction): Routes { // Legacy staking Pre v14 pallet version. stakingLegacy(t), collator(t), + coretime(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..33e68d2e30fd 100644 --- a/packages/apps-routing/tsconfig.build.json +++ b/packages/apps-routing/tsconfig.build.json @@ -14,6 +14,8 @@ { "path": "../page-bounties/tsconfig.build.json" }, { "path": "../page-calendar/tsconfig.build.json" }, { "path": "../page-claims/tsconfig.build.json" }, + { "path": "../page-coretime/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/app-coretime.json b/packages/apps/public/locales/en/app-coretime.json new file mode 100644 index 000000000000..fca8fb4c065d --- /dev/null +++ b/packages/apps/public/locales/en/app-coretime.json @@ -0,0 +1,26 @@ +{ + "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", + "mask": "mask", + "nav.coretime": "Coretime", + "next index": "next index", + "pool size": "pool size", + "selected core for workload": "selected core for workload", + "selected core for workplan": "selected core for workplan", + "timeslice": "timeslice", + "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..afeac98eba19 100644 --- a/packages/apps/public/locales/en/index.json +++ b/packages/apps/public/locales/en/index.json @@ -4,10 +4,12 @@ "app-alliance.json", "app-assets.json", "app-bounties.json", + "app-broker.json", "app-calendar.json", "app-claims.json", "app-collator.json", "app-contracts.json", + "app-coretime.json", "app-council.json", "app-democracy.json", "app-explorer.json", diff --git a/packages/apps/public/locales/en/translation.json b/packages/apps/public/locales/en/translation.json index 3333c2bae966..f5c382bb1d22 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": "", @@ -1625,6 +1641,7 @@ "referendum id": "", "refresh in": "", "refund from account": "", + "region length": "", "register from": "", "registrar account": "", "registrar index": "", @@ -1660,6 +1677,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": "", @@ -1734,6 +1754,8 @@ "the supplied signature": "", "threshold": "", "timeout": "", + "timeslice": "", + "timeslice period": "", "tip": "", "tip amount": "", "tip reason": "", @@ -1756,6 +1778,8 @@ "total sub": "", "total transferable": "", "track origin": "", + "traffic": "", + "traffic multiplier": "", "transactions": "", "transfer asset": "", "transfer received": "", @@ -1840,6 +1864,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..7e7783a4572d --- /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-11-x", + "dependencies": { + "@polkadot/react-components": "0.143.3-11-x", + "@polkadot/react-query": "0.143.3-11-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..dc8490cf0695 --- /dev/null +++ b/packages/page-broker/src/Overview/CoreTable.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 { CoreWorkloadInfo, CoreWorkplanInfo } from '@polkadot/react-hooks/types'; + +import React, { useRef } from 'react'; + +import { Table } from '@polkadot/react-components'; + +import { useTranslation } from '../translate.js'; +import Workload from './Workload.js'; + +interface Props { + api: ApiPromise; + core: number; + workload?: CoreWorkloadInfo[], + workplan?: CoreWorkplanInfo[], + 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 = [[
{headerRef.current} {core}
, 'core', 8]]; + + return ( + + {workload?.map((v) => ( + + ))} +
+ ); +} + +export default React.memo(CoreTable); diff --git a/packages/page-broker/src/Overview/Cores.tsx b/packages/page-broker/src/Overview/Cores.tsx new file mode 100644 index 000000000000..2e15228a741d --- /dev/null +++ b/packages/page-broker/src/Overview/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/CoresTables.tsx b/packages/page-broker/src/Overview/CoresTables.tsx new file mode 100644 index 000000000000..7a3df557f417 --- /dev/null +++ b/packages/page-broker/src/Overview/CoresTables.tsx @@ -0,0 +1,59 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { CoreWorkloadInfo, CoreWorkplanInfo } from '@polkadot/react-hooks/types'; +import type { CoreInfo } from '../types.js'; + +import React from 'react'; + +import { sortByCore } from '../utils.js'; +import CoreTable from './CoreTable.js'; + +interface Props { + api: ApiPromise; + cores?: number; + workloadInfos?: CoreWorkloadInfo[] | CoreWorkloadInfo; + workplanInfos?: CoreWorkplanInfo[] | CoreWorkplanInfo; + timeslice: number; +} + +function CoresTable ({ api, cores, timeslice, workloadInfos, workplanInfos }: Props): React.ReactElement { + const coreArr = []; + const sanitizedLoad: CoreWorkloadInfo[] = sortByCore(workloadInfos); + const sanitizedPlan: CoreWorkplanInfo[] = sortByCore(workplanInfos); + + if (cores === -1 && !!sanitizedLoad) { + coreArr.push(...sanitizedLoad.map((plan) => plan.core)); + } else if (cores !== undefined) { + coreArr.push(cores); + } + + const filteredList: CoreInfo[] = coreArr.map((c) => ({ + core: c, + workload: sanitizedLoad.filter((v) => v.core === c), + workplan: sanitizedPlan.filter((v) => v.core === c) + })); + + return ( + <> + { + filteredList.map((c) => { + return ( + + ); + } + ) + } + + ); +} + +export default React.memo(CoresTable); diff --git a/packages/page-broker/src/Overview/RegionLength.tsx b/packages/page-broker/src/Overview/RegionLength.tsx new file mode 100644 index 000000000000..130010659b87 --- /dev/null +++ b/packages/page-broker/src/Overview/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/RenewalPrice.tsx b/packages/page-broker/src/Overview/RenewalPrice.tsx new file mode 100644 index 000000000000..df34e3d1bc4c --- /dev/null +++ b/packages/page-broker/src/Overview/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.tsx b/packages/page-broker/src/Overview/Summary.tsx new file mode 100644 index 000000000000..ac149fd27674 --- /dev/null +++ b/packages/page-broker/src/Overview/Summary.tsx @@ -0,0 +1,94 @@ +// 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 { CoreWorkloadInfo } from '@polkadot/react-hooks/types'; +import type { 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 './Cores.js'; +import RegionLength from './RegionLength.js'; +import RenewalPrice from './RenewalPrice.js'; +import Timeslice from './Timeslice.js'; +import TimeslicePeriod from './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?: CoreWorkloadInfo[] | CoreWorkloadInfo +} + +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/Timeslice.tsx b/packages/page-broker/src/Overview/Timeslice.tsx new file mode 100644 index 000000000000..a8f3546d3bee --- /dev/null +++ b/packages/page-broker/src/Overview/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/TimeslicePeriod.tsx b/packages/page-broker/src/Overview/TimeslicePeriod.tsx new file mode 100644 index 000000000000..2581778ae1a4 --- /dev/null +++ b/packages/page-broker/src/Overview/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..5d55da49718b --- /dev/null +++ b/packages/page-broker/src/Overview/WorkInfoRow.tsx @@ -0,0 +1,87 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { InfoRow } from '../types.js'; + +import React from 'react'; + +import { AddressMini, styled } from '@polkadot/react-components'; + +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 { + const NoTaskAssigned = !data.taskId; + + if (NoTaskAssigned) { + return ( + <> + no task assigned + + ); + } + + 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..46fb05a708b1 --- /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 { CoreWorkloadInfo, CoreWorkplanInfo, RegionInfo } from '@polkadot/react-hooks/types'; +import type { InfoRow } from '../types.js'; + +import React, { useEffect, useState } from 'react'; + +import { ExpandButton } from '@polkadot/react-components'; +import { useRegions, useToggle } from '@polkadot/react-hooks'; + +import { formatWorkInfo } from '../utils.js'; +import WorkInfoRow from './WorkInfoRow.js'; +import Workplan from './Workplan.js'; + +interface Props { + api: ApiPromise; + value: CoreWorkloadInfo; + timeslice: number; + workplan?: CoreWorkplanInfo[] | null, +} + +function Workload ({ api, timeslice, value: { core, info }, 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(formatWorkInfo(info, core, region, timeslice)); + setCurrentRegion(region); + } + }, [info, regionInfo, core, timeslice]); + + const hasWorkplan = !!workplan?.length; + + return ( + <> + { + tableData?.map((data) => ( + + + +
Workplan
+ {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..3b1d169ff31f --- /dev/null +++ b/packages/page-broker/src/Overview/Workplan.tsx @@ -0,0 +1,58 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CoreWorkplanInfo, RegionInfo } from '@polkadot/react-hooks/types'; +import type { InfoRow } from '../types.js'; + +import React, { useEffect, useState } from 'react'; + +import { Spinner } from '@polkadot/react-components'; + +import { formatWorkInfo } from '../utils.js'; +import WorkInfoRow from './WorkInfoRow.js'; + +interface Props { + className?: string; + value: CoreWorkplanInfo; + currentTimeSlice: number + isExpanded: boolean + region: RegionInfo | undefined +} + +function Workplan ({ currentTimeSlice, isExpanded, region, value: { core, info } }: Props): React.ReactElement { + const [tableData, setTableData] = useState(); + + useEffect(() => { + setTableData(formatWorkInfo(info, core, region, currentTimeSlice)); + }, [info, region, core, currentTimeSlice]); + + if (!tableData?.length) { + return ( + + + + + ); + } + + return ( + <> + {tableData?.map((data) => ( + + + + + ))} + + + ); +} + +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..f0960b3b52f7 --- /dev/null +++ b/packages/page-broker/src/Overview/index.tsx @@ -0,0 +1,126 @@ +// 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 { CoreDescription, CoreWorkloadInfo, CoreWorkplanInfo } from '@polkadot/react-hooks/types'; +import type { PalletBrokerStatusRecord } from '@polkadot/types/lookup'; + +import React, { useEffect, useMemo, useState } from 'react'; + +import { Dropdown, Input, styled } from '@polkadot/react-components'; +import { useCall, useDebounce } from '@polkadot/react-hooks'; + +import { useTranslation } from '../translate.js'; +import CoresTable from './CoresTables.js'; +import Summary from './Summary.js'; + +const StyledDiv = styled.div` + @media (max-width: 768px) { + max-width: 100%: + } +`; + +interface Props { + className?: string; + workloadInfos?: CoreWorkloadInfo[]; + workplanInfos?: CoreWorkplanInfo[]; + coreInfos?: CoreDescription[]; + apiEndpoint?: LinkOption | null; + api: ApiPromise; + isReady: boolean; +} + +const filterLoad = (parachainId: string, load: CoreWorkloadInfo[] | CoreWorkplanInfo[], workloadCoreSelected: number) => { + if (parachainId) { + return load?.filter(({ info }) => info?.[0]?.assignment.isTask ? info?.[0]?.assignment.asTask.toString() === parachainId : false); + } + + if (workloadCoreSelected === -1) { + return load; + } + + return load?.filter(({ core }) => core === workloadCoreSelected); +}; + +function Overview ({ api, apiEndpoint, className, isReady, workloadInfos, workplanInfos }: Props): React.ReactElement { + const { t } = useTranslation(); + const [workloadCoreSelected, setWorkloadCoreSelected] = useState(-1); + const [_parachainId, setParachainId] = useState(''); + const parachainId = useDebounce(_parachainId); + const [coreArr, setCoreArr] = useState([]); + + useEffect(() => { + const newCoreArr = Array.from({ length: workloadInfos?.length || 0 }, (_, index) => index); + + setCoreArr(newCoreArr); + }, [workloadInfos]); + + const workloadCoreOpts = useMemo( + () => [{ 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] + ); + + const filteredWLC = useMemo( + () => workloadInfos && filterLoad(parachainId, workloadInfos, workloadCoreSelected), + [workloadInfos, workloadCoreSelected, parachainId] + ); + + function onDropDownChange (v: number) { + setWorkloadCoreSelected(v); + setParachainId(''); + } + + function onInputChange (v: string) { + setParachainId(v); + setWorkloadCoreSelected(-1); + } + + const status = useCall(isReady && api.query.broker?.status); + const timeslice = status?.toHuman().lastCommittedTimeslice?.toString(); + const timesliceAsString = timeslice === undefined ? '' : timeslice.toString().split(',').join(''); + + 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..74e4aa32cd78 --- /dev/null +++ b/packages/page-broker/src/index.tsx @@ -0,0 +1,53 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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, useWorkloadInfos, useWorkplanInfos } 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(); + const workloadInfos = useWorkloadInfos(api, isApiReady); + const workplanInfos = useWorkplanInfos(api, isApiReady); + + 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..b52b15610ba1 --- /dev/null +++ b/packages/page-broker/src/types.ts @@ -0,0 +1,28 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CoreWorkloadInfo, CoreWorkplanInfo } from '@polkadot/react-hooks/types'; + +export interface InfoRow { + taskId: string | null, + maskBits: number, + core: number + mask?: string + start?: string | null, + end?: string | null + owner?: string + leaseLength?: number + endBlock?: number +} + +export interface CoreInfo { + core: number, + workload: CoreWorkloadInfo[], + workplan: CoreWorkplanInfo[] +} + +export interface statsType { + idles: number, + pools: number, + tasks: number +} diff --git a/packages/page-broker/src/utils.ts b/packages/page-broker/src/utils.ts new file mode 100644 index 000000000000..3ed0ee7ea1a9 --- /dev/null +++ b/packages/page-broker/src/utils.ts @@ -0,0 +1,124 @@ +// Copyright 2017-2024 @polkadot/app-broker authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CoreWorkloadInfo, RegionInfo } from '@polkadot/react-hooks/types'; +import type { PalletBrokerScheduleItem } from '@polkadot/types/lookup'; +import type { InfoRow } from './types.js'; + +import { BN } from '@polkadot/util'; + +export function hexToBin (hex: string): string { + return parseInt(hex, 16).toString(2); +} + +export function processHexMask (mask: PalletBrokerScheduleItem['mask']) { + 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; +} + +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(6000); // Average block time in milliseconds (6 seconds) + const timeSlice = new BN(80); + 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); + + let estTimestamp; + + if (targetBlockBN.lt(latestBlockBN)) { + estTimestamp = timestampBN.sub(blockDifference); + } else { + estTimestamp = timestampBN.add(blockDifference); + } + + return formatDate(new Date(estTimestamp.toNumber())); + } catch (error) { + console.error('Error in calculation:', error); + + return null; + } +}; + +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); +} + +export function formatWorkInfo (info: PalletBrokerScheduleItem[], core: number, currentRegion: RegionInfo | undefined, timeslice: number) { + const infoVec: InfoRow[] = []; + + info.forEach((data) => { + const maskBits: number = processHexMask(data.mask).length; + const isPool = data?.assignment.isPool; + const taskId = data?.assignment.isTask ? data?.assignment.asTask.toString() : isPool ? 'Pool' : ''; + const item: InfoRow = { core, maskBits, taskId }; + + 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(); + } + + infoVec.push(item); + }); + + return infoVec; +} + +export function getStats (totalCores: string | undefined, workloadInfos: CoreWorkloadInfo[] | CoreWorkloadInfo | undefined) { + if (!totalCores || !workloadInfos) { + return { idles: 0, pools: 0, tasks: 0 }; + } + + const sanitized: CoreWorkloadInfo[] = Array.isArray(workloadInfos) ? workloadInfos : [workloadInfos]; + + const { pools, tasks } = sanitized.reduce( + (acc, { info }) => { + if (info[0].assignment.isTask) { + acc.tasks += 1; + } else if (info[0].assignment.isPool) { + acc.pools += 1; + } + + return acc; + }, + { pools: 0, tasks: 0 } + ); + const idles = Number(totalCores) - (pools + tasks); + + return { idles, pools, tasks }; +} 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/page-coretime/README.md b/packages/page-coretime/README.md new file mode 100644 index 000000000000..d9c23d4732fd --- /dev/null +++ b/packages/page-coretime/README.md @@ -0,0 +1 @@ +# @polkadot/app-coretime diff --git a/packages/page-coretime/package.json b/packages/page-coretime/package.json new file mode 100644 index 000000000000..8a1dc22e3620 --- /dev/null +++ b/packages/page-coretime/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-coretime#readme", + "license": "Apache-2.0", + "name": "@polkadot/app-coretime", + "private": true, + "repository": { + "directory": "packages/page-coretime", + "type": "git", + "url": "https://github.com/polkadot-js/apps.git" + }, + "sideEffects": false, + "type": "module", + "version": "0.143.3-11-x", + "dependencies": { + "@polkadot/react-components": "0.143.3-11-x", + "@polkadot/react-query": "0.143.3-11-x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*", + "react-is": "*" + } +} diff --git a/packages/page-coretime/src/Overview/BrokerId.tsx b/packages/page-coretime/src/Overview/BrokerId.tsx new file mode 100644 index 000000000000..622a0e3471d1 --- /dev/null +++ b/packages/page-coretime/src/Overview/BrokerId.tsx @@ -0,0 +1,27 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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 id = api.consts.coretime?.brokerId as u32; + + return ( +
+ {id?.toString()} + {children} +
+ ); +} + +export default React.memo(BrokerId); diff --git a/packages/page-coretime/src/Overview/CoreDescriptor.tsx b/packages/page-coretime/src/Overview/CoreDescriptor.tsx new file mode 100644 index 000000000000..3704e938ea88 --- /dev/null +++ b/packages/page-coretime/src/Overview/CoreDescriptor.tsx @@ -0,0 +1,66 @@ +// Copyright 2017-2024 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CoreDescription } from '@polkadot/react-hooks/types'; +import type { PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor } from '@polkadot/types/lookup'; + +import React from 'react'; + +import { styled } from '@polkadot/react-components'; + +import CoreQueue from './CoreQueue.js'; +import CurrentWork from './CurrentWork.js'; + +interface Props { + className?: string; + value: CoreDescription; +} + +function CoreDescriptor ({ className, value: { core, info } }: Props): React.ReactElement { + let sanitized: PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor[] = []; + + if (Array.isArray(info)) { + sanitized = info; + } else if (info) { + sanitized.push(info); + } + + return ( + <> + + {sanitized?.map((i) => ( + + + + )) + } + {sanitized?.map((i) => ( + + + + ))} + + + ); +} + +const StyledTr = styled.tr` + .shortHash { + max-width: var(--width-shorthash); + min-width: 3em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: var(--width-shorthash); + } +`; + +export default React.memo(CoreDescriptor); diff --git a/packages/page-coretime/src/Overview/CoreDescriptors.tsx b/packages/page-coretime/src/Overview/CoreDescriptors.tsx new file mode 100644 index 000000000000..2e63e28186cd --- /dev/null +++ b/packages/page-coretime/src/Overview/CoreDescriptors.tsx @@ -0,0 +1,67 @@ +// Copyright 2017-2024 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CoreDescription } from '@polkadot/react-hooks/types'; + +import React, { useMemo } from 'react'; + +import { ExpandButton, Table } from '@polkadot/react-components'; +import { useToggle } from '@polkadot/react-hooks'; + +import { useTranslation } from '../translate.js'; +import CoreDescriptor from './CoreDescriptor.js'; + +interface Props { + className?: string; + coreInfos?: CoreDescription; +} + +function CoreDescriptors ({ className, coreInfos }: Props): React.ReactElement { + const { t } = useTranslation(); + const [isExpanded, toggleExpanded] = useToggle(); + + const [headerButton, headerChildren] = useMemo( + () => [ + false && coreInfos && ( + + ), + isExpanded && coreInfos && ( + + + + ) + ], + [isExpanded, toggleExpanded, coreInfos] + ); + + const [header, key] = useMemo( + (): [([React.ReactNode?, string?, number?] | null)[], string] => [ + [ + [<>{t('core')}
{coreInfos?.core}
, 'start', 8], + null && [headerButton] + ], + 'core' + ], + [headerButton, t, coreInfos] + ); + + return ( + + {coreInfos && } +
+ ); +} + +export default React.memo(CoreDescriptors); diff --git a/packages/page-coretime/src/Overview/CoreQueue.tsx b/packages/page-coretime/src/Overview/CoreQueue.tsx new file mode 100644 index 000000000000..f2f4567a9b2e --- /dev/null +++ b/packages/page-coretime/src/Overview/CoreQueue.tsx @@ -0,0 +1,41 @@ +// Copyright 2017-2024 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PolkadotRuntimeParachainsAssignerCoretimeQueueDescriptor } from '@polkadot/types/lookup'; + +import React, { useRef } from 'react'; + +import { Table } from '@polkadot/react-components'; + +interface Props { + value?: PolkadotRuntimeParachainsAssignerCoretimeQueueDescriptor; +} + +function CoreQueue ({ value }: Props): React.ReactElement { + const headerRef = useRef<([React.ReactNode?, string?] | false)[]>([ + ['work queue'] + ]); + + return ( + + {value + ? + + + + : + + } +
+
{'first'}
+ {value?.first.toString()} +
+
{'last'}
+ {value?.last.toString()} +
+
{'No work queue found'}
+
+ ); +} + +export default React.memo(CoreQueue); diff --git a/packages/page-coretime/src/Overview/CurrentWork.tsx b/packages/page-coretime/src/Overview/CurrentWork.tsx new file mode 100644 index 000000000000..6465a6dc7d82 --- /dev/null +++ b/packages/page-coretime/src/Overview/CurrentWork.tsx @@ -0,0 +1,62 @@ +// Copyright 2017-2024 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PolkadotRuntimeParachainsAssignerCoretimeWorkState } from '@polkadot/types/lookup'; + +import React, { useEffect, useRef, useState } from 'react'; + +import { Table } from '@polkadot/react-components'; + +interface Props { + value?: PolkadotRuntimeParachainsAssignerCoretimeWorkState; +} + +function createAssignments (value?: PolkadotRuntimeParachainsAssignerCoretimeWorkState): string { + if (value) { + if (value.assignments.length > 1) { + return value.assignments.map((_, index) => { + const ratio = value.assignments[index][1].ratio.toNumber() / 57600 * 100; + + if (value.assignments[index][0].isIdle) { + return `${ratio}% Idle`; + } else if (value.assignments[index][0].isPool) { + return `${ratio}% Pool`; + } else { + return `${ratio}% Task: ${value.assignments[index][0].asTask.toString()}`; + } + }).join(', '); + } else { + if (value.assignments[0][0].isIdle) { + return '100% Idle'; + } else if (value.assignments[0][0].isPool) { + return '100% Pool'; + } else { + return `100% Task: ${value.assignments[0][0].asTask.toString()}`; + } + } + } else { + return 'Queue empty'; + } +} + +function CurrentWork ({ value }: Props): React.ReactElement { + const [assignments, setAssignments] = useState(''); + + useEffect(() => { + setAssignments(createAssignments(value)); + }, [value]); + + const headerRef = useRef<([React.ReactNode?, string?] | false)[]>([ + ['current work'] + ]); + + return ( + + + + +
{assignments}
+ ); +} + +export default React.memo(CurrentWork); diff --git a/packages/page-coretime/src/Overview/QueueStatus.tsx b/packages/page-coretime/src/Overview/QueueStatus.tsx new file mode 100644 index 000000000000..16dad860c2fc --- /dev/null +++ b/packages/page-coretime/src/Overview/QueueStatus.tsx @@ -0,0 +1,35 @@ +// Copyright 2017-2024 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { OnDemandQueueStatus } from '@polkadot/react-hooks/types'; + +import React from 'react'; + +import { FormatBalance } from '@polkadot/react-query'; + +interface Props { + className?: string; + value?: OnDemandQueueStatus; + query: string; +} + +function QueueStatus ({ className, query, value }: Props): React.ReactElement { + return ( + <> + {value && query === 'traffic' + ?
+ +
+ : value && +
+ {value?.nextIndex.toString()} +
} + + ); +} + +export default React.memo(QueueStatus); diff --git a/packages/page-coretime/src/Overview/Summary.tsx b/packages/page-coretime/src/Overview/Summary.tsx new file mode 100644 index 000000000000..bffd40dc86d4 --- /dev/null +++ b/packages/page-coretime/src/Overview/Summary.tsx @@ -0,0 +1,57 @@ +// Copyright 2017-2024 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CoreDescription, OnDemandQueueStatus } from '@polkadot/react-hooks/types'; + +import React from 'react'; + +import { CardSummary, SummaryBox, UsageBar } from '@polkadot/react-components'; +import { useApi, useQueueStatus } from '@polkadot/react-hooks'; + +import { useTranslation } from '../translate.js'; +import BrokerId from './BrokerId.js'; +import QueueStatus from './QueueStatus.js'; + +interface Props { + coreDscriptors?: CoreDescription[]; +} + +function Summary ({ coreDscriptors }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api, apiEndpoint } = useApi(); + const queueStatus: OnDemandQueueStatus | undefined = useQueueStatus(); + + return ( + +
+ <> + {api.query.coretimeAssignmentProvider && + + + } + {api.query.onDemandAssignmentProvider.queueStatus && + <> + + + + + + + } + + +
+
+ ); +} + +export default React.memo(Summary); diff --git a/packages/page-coretime/src/Overview/index.tsx b/packages/page-coretime/src/Overview/index.tsx new file mode 100644 index 000000000000..ab87570c6c7a --- /dev/null +++ b/packages/page-coretime/src/Overview/index.tsx @@ -0,0 +1,34 @@ +// Copyright 2017-2024 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CoreDescription } from '@polkadot/react-hooks/types'; + +import React from 'react'; + +import CoreDescriptors from './CoreDescriptors.js'; +import Summary from './Summary.js'; + +interface Props { + className?: string; + coreInfos?: CoreDescription[]; +} + +function Overview ({ className, coreInfos }: Props): React.ReactElement { + return ( +
+ + { + coreInfos?.map((v) => ( + + )) + } +
+ ); +} + +export default React.memo(Overview); diff --git a/packages/page-coretime/src/index.tsx b/packages/page-coretime/src/index.tsx new file mode 100644 index 000000000000..77bed4cad06c --- /dev/null +++ b/packages/page-coretime/src/index.tsx @@ -0,0 +1,48 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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, useCoreDescriptor } 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 CoretimeApp ({ basePath, className }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api, isApiReady } = useApi(); + const itemsRef = useRef(createItemsRef(t)); + const coreInfos = useCoreDescriptor(api, isApiReady); + + return ( +
+ + +
+ ); +} + +export default React.memo(CoretimeApp); diff --git a/packages/page-coretime/src/translate.ts b/packages/page-coretime/src/translate.ts new file mode 100644 index 000000000000..125dda4ab07d --- /dev/null +++ b/packages/page-coretime/src/translate.ts @@ -0,0 +1,8 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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-coretime'); +} diff --git a/packages/page-coretime/tsconfig.build.json b/packages/page-coretime/tsconfig.build.json new file mode 100644 index 000000000000..0292e5e4c9fe --- /dev/null +++ b/packages/page-coretime/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/MaskCoverage.tsx b/packages/react-components/src/MaskCoverage.tsx new file mode 100644 index 000000000000..fa7639c3fe5c --- /dev/null +++ b/packages/react-components/src/MaskCoverage.tsx @@ -0,0 +1,44 @@ +// Copyright 2017-2024 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { styled } from './styled.js'; + +const BarContainer = styled.div` + display: flex; + align-items: center; + border-radius: 4px; + overflow: hidden; +`; + +interface SectionProps { + value: string; +} + +const Segment = styled.div` + flex: 1; + width: 5px; + height: 20px; + margin-right: 2px; + opacity: ${(props) => { + return props.value; + }}; + width: 100%; + background-color: ${(props) => props.value === '1' ? 'var(--bg-inverse)' : 'white'}; +`; + +function MaskCoverage ({ values }: { values: string[] }) { + return ( + + {values.map((value, index) => ( + + ))} + + ); +} + +export default React.memo(MaskCoverage); diff --git a/packages/react-components/src/UsageBar.tsx b/packages/react-components/src/UsageBar.tsx new file mode 100644 index 000000000000..59e4ae8e5e10 --- /dev/null +++ b/packages/react-components/src/UsageBar.tsx @@ -0,0 +1,107 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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..1878f2f0077c 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -66,6 +66,7 @@ export { default as LinkExternal } from './LinkExternal.js'; export { default as LockedVote } from './LockedVote.js'; export { default as MarkError } from './MarkError.js'; export { default as MarkWarning } from './MarkWarning.js'; +export { default as MaskCoverage } from './MaskCoverage.js'; export { default as Menu } from './Menu/index.js'; export { default as Modal } from './Modal/index.js'; export { default as NextTick } from './NextTick.js'; @@ -96,6 +97,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..5e7ccb717c6f 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -22,11 +22,14 @@ export { useBlockEvents } from './useBlockEvents.js'; export { useBlockInterval } from './useBlockInterval.js'; export { useBlocksPerDays } from './useBlocksPerDays.js'; export { useBlockTime } from './useBlockTime.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 +69,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 +90,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..0cc767f99656 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 { PalletBrokerScheduleItem, 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,35 @@ 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 CoreWorkloadInfo { + core: number; + info: PalletBrokerScheduleItem[]; +} + +export interface CoreWorkplanInfo { + timeslice: number; + core: number; + info: PalletBrokerScheduleItem[]; +} + +export interface RegionInfo { + core: number, + start: number, + end: number, + owner: string, + paid: string, + mask: `0x${string}` +} 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..16fa404a3771 --- /dev/null +++ b/packages/react-hooks/src/useCoreDescriptor.ts @@ -0,0 +1,51 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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..8412abd516cb --- /dev/null +++ b/packages/react-hooks/src/useCurrentPrice.tsx @@ -0,0 +1,31 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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..42bf4b9d7d73 --- /dev/null +++ b/packages/react-hooks/src/useQueueStatus.ts @@ -0,0 +1,36 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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..302378b2addf --- /dev/null +++ b/packages/react-hooks/src/useRegions.ts @@ -0,0 +1,49 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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..aaffd9d01486 --- /dev/null +++ b/packages/react-hooks/src/useRenewalBump.ts @@ -0,0 +1,31 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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..8521aa2042c6 --- /dev/null +++ b/packages/react-hooks/src/useWorkloadInfos.ts @@ -0,0 +1,44 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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 { CoreWorkloadInfo } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useCall, useMapKeys } from '@polkadot/react-hooks'; + +function extractInfo (info: PalletBrokerScheduleItem[], core: number): CoreWorkloadInfo { + return { + core, + info + }; +} + +const OPT_KEY = { + transform: (keys: StorageKey<[u32]>[]): u32[] => + keys.map(({ args: [core] }) => core) +}; + +function useWorkloadInfosImpl (api: ApiPromise, ready: boolean): CoreWorkloadInfo[] | 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 => { + workloadInfo && + setState( + workloadInfo[0][0].map((info, index) => + extractInfo(workloadInfo[1][index], info.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..2f4944d744e8 --- /dev/null +++ b/packages/react-hooks/src/useWorkplanInfos.ts @@ -0,0 +1,49 @@ +// Copyright 2017-2024 @polkadot/app-coretime 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 { CoreWorkplanInfo } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useCall, useMapKeys } from '@polkadot/react-hooks'; + +function extractInfo (info: Vec, timeslice: number, core: number) { + return { + core, + info, + timeslice + }; +} + +const OPT_KEY = { + transform: (keys: StorageKey<[u32, u16]>[]): [u32, u16][] => + keys.map(({ args: [timeslice, core] }) => [timeslice, core]) +}; + +function useWorkplanInfosImpl (api: ApiPromise, ready: boolean): CoreWorkplanInfo[] | 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((): void => { + workplanInfo?.[1] && + setState( + workplanInfo[0][0].map((info, index) => + extractInfo(workplanInfo[1][index].unwrap(), info[0].toNumber(), info[1].toNumber()) + ) + ); + }, [workplanInfo]); + + return state; +} + +export const useWorkplanInfos = createNamedHook('useWorkplanInfos', useWorkplanInfosImpl); 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..6eca878c6a25 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -25,6 +25,8 @@ "@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-coretime": ["page-coretime/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..9f6ee5b99ccd 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -21,11 +21,13 @@ { "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" }, { "path": "./packages/page-collator/tsconfig.build.json" }, { "path": "./packages/page-contracts/tsconfig.build.json" }, + { "path": "./packages/page-coretime/tsconfig.build.json" }, { "path": "./packages/page-council/tsconfig.build.json" }, { "path": "./packages/page-democracy/tsconfig.build.json" }, { "path": "./packages/page-explorer/tsconfig.build.json" }, diff --git a/yarn.lock b/yarn.lock index 070202892a3c..07cb2401ef57 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-11-x" + "@polkadot/react-query": "npm:0.143.3-11-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" @@ -1391,6 +1404,19 @@ __metadata: languageName: unknown linkType: soft +"@polkadot/app-coretime@workspace:packages/page-coretime": + version: 0.0.0-use.local + resolution: "@polkadot/app-coretime@workspace:packages/page-coretime" + dependencies: + "@polkadot/react-components": "npm:0.143.3-11-x" + "@polkadot/react-query": "npm:0.143.3-11-x" + peerDependencies: + react: "*" + react-dom: "*" + react-is: "*" + languageName: unknown + linkType: soft + "@polkadot/app-council@workspace:packages/page-council": version: 0.0.0-use.local resolution: "@polkadot/app-council@workspace:packages/page-council" @@ -2122,7 +2148,7 @@ __metadata: languageName: unknown linkType: soft -"@polkadot/react-components@npm:^0.143.3-11-x, @polkadot/react-components@workspace:packages/react-components": +"@polkadot/react-components@npm:0.143.3-11-x, @polkadot/react-components@npm:^0.143.3-11-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 +2272,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/react-query@npm:^0.143.3-11-x, @polkadot/react-query@workspace:packages/react-query": +"@polkadot/react-query@npm:0.143.3-11-x, @polkadot/react-query@npm:^0.143.3-11-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 +2931,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 +3337,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 +10248,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