Coretime Broker on Coretime Chain - Update after Coretime Polkadot release (#10947)
* showing all cores, not relying on the core information in workload to show the cores * swapped idles and pools to reflect correct values * added start and end for bulk coretime, taking into account timeslice from the workplan * checked and updated the time estimation function * added cores offerred vs sold in summary * filters turned back on * lint * reversed a change in a file from .github folder * removed some extra checks * minor refactor
This commit is contained in:
parent
f8efe68d31
commit
859ba4800e
@ -16,10 +16,9 @@ interface Props {
|
||||
core: number;
|
||||
workload?: CoreWorkloadType[],
|
||||
workplan?: CoreWorkplanType[],
|
||||
timeslice: number,
|
||||
}
|
||||
|
||||
function CoreTable ({ api, core, timeslice, workload, workplan }: Props): React.ReactElement<Props> {
|
||||
function CoreTable ({ api, core, workload, workplan }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const headerRef = useRef<([React.ReactNode?, string?] | false)[]>([[t('core')]]);
|
||||
const header: [React.ReactNode?, string?, number?, (() => void)?][] = [
|
||||
@ -38,15 +37,14 @@ function CoreTable ({ api, core, timeslice, workload, workplan }: Props): React.
|
||||
isSplit={false}
|
||||
key={core}
|
||||
>
|
||||
{workload?.map((v) => (
|
||||
<Workload
|
||||
api={api}
|
||||
key={v.core}
|
||||
timeslice={timeslice}
|
||||
value={v}
|
||||
workplan={workplan}
|
||||
/>
|
||||
))}
|
||||
<Workload
|
||||
api={api}
|
||||
core={core}
|
||||
key={core}
|
||||
workload={workload}
|
||||
workplan={workplan}
|
||||
/>
|
||||
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,44 +2,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { CoreInfo, CoreWorkloadType, CoreWorkplanType } from '../types.js';
|
||||
import type { CoreInfo } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import CoreTable from './CoreTable.js';
|
||||
|
||||
interface Props {
|
||||
api: ApiPromise;
|
||||
workloadInfos?: CoreWorkloadType[];
|
||||
workplanInfos?: CoreWorkplanType[];
|
||||
timeslice: number;
|
||||
data: CoreInfo[];
|
||||
}
|
||||
|
||||
function CoresTable ({ api, timeslice, workloadInfos, workplanInfos }: Props): React.ReactElement<Props> {
|
||||
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]);
|
||||
|
||||
function CoresTable ({ api, data }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
filteredList.map((c) => {
|
||||
return (
|
||||
<CoreTable
|
||||
api={api}
|
||||
core={c.core}
|
||||
key={`core ${c.core}`}
|
||||
timeslice={timeslice}
|
||||
workload={c.workload}
|
||||
workplan={c.workplan}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
{data?.map((coreData) => {
|
||||
return (
|
||||
<CoreTable
|
||||
api={api}
|
||||
core={coreData?.core}
|
||||
key={coreData?.core}
|
||||
workload={coreData?.workload}
|
||||
workplan={coreData?.workplan}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
// Copyright 2017-2024 @polkadot/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CoreInfo } from '../types.js';
|
||||
|
||||
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) {
|
||||
@ -16,31 +17,31 @@ const StyledDiv = styled.div`
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
workLoad?: CoreWorkloadType[];
|
||||
onFilter: (data: CoreWorkloadType[]) => void
|
||||
data: CoreInfo[];
|
||||
onFilter: (data: CoreInfo[]) => void
|
||||
}
|
||||
|
||||
const filterLoad = (parachainId: string, load: CoreWorkloadType[] | CoreWorkplanType[], workloadCoreSelected: number): CoreWorkloadType[] | CoreWorkplanType[] => {
|
||||
const filterLoad = (parachainId: string, data: CoreInfo[], workloadCoreSelected: number): CoreInfo[] => {
|
||||
if (parachainId) {
|
||||
return load?.filter(({ info }) => info.task === parachainId);
|
||||
return data.filter(({ workload }) => !!workload?.filter(({ info }) => info.task === parachainId).length);
|
||||
}
|
||||
|
||||
if (workloadCoreSelected === -1) {
|
||||
return load;
|
||||
return data;
|
||||
}
|
||||
|
||||
return load?.filter(({ core }) => core === workloadCoreSelected);
|
||||
return data.filter((one) => one.core === workloadCoreSelected);
|
||||
};
|
||||
|
||||
function Filters ({ onFilter, workLoad }: Props): React.ReactElement<Props> {
|
||||
function Filters ({ data, onFilter }: Props): React.ReactElement<Props> {
|
||||
const [workloadCoreSelected, setWorkloadCoreSelected] = useState(-1);
|
||||
const [_parachainId, setParachainId] = useState<string>('');
|
||||
|
||||
const coreArr: number[] = useMemo(() =>
|
||||
workLoad?.length
|
||||
? Array.from({ length: workLoad?.length || 0 }, (_, index) => index)
|
||||
data?.length
|
||||
? Array.from({ length: data.length || 0 }, (_, index) => index)
|
||||
: []
|
||||
, [workLoad]);
|
||||
, [data]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const parachainId = useDebounce(_parachainId);
|
||||
@ -60,14 +61,14 @@ function Filters ({ onFilter, workLoad }: Props): React.ReactElement<Props> {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workLoad) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = filterLoad(parachainId, workLoad, workloadCoreSelected);
|
||||
const filtered = filterLoad(parachainId, data, workloadCoreSelected);
|
||||
|
||||
onFilter(filtered);
|
||||
}, [workLoad, workloadCoreSelected, parachainId, onFilter]);
|
||||
}, [data, workloadCoreSelected, parachainId, onFilter]);
|
||||
|
||||
const onDropDownChange = useCallback((v: number) => {
|
||||
setWorkloadCoreSelected(v);
|
||||
|
||||
@ -2,19 +2,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
|
||||
import type { CoreWorkloadType, statsType } from '../types.js';
|
||||
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 } from '@polkadot/react-hooks';
|
||||
import { useApi, useBrokerSalesInfo, useBrokerStatus } from '@polkadot/react-hooks';
|
||||
import { type CoreWorkload } from '@polkadot/react-hooks/types';
|
||||
import { formatBalance } from '@polkadot/util';
|
||||
|
||||
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';
|
||||
|
||||
@ -34,37 +34,47 @@ const StyledSection = styled.section`
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
coreCount?: string
|
||||
apiEndpoint?: LinkOption | null;
|
||||
workloadInfos?: CoreWorkloadType[] | CoreWorkloadType
|
||||
workloadInfos?: CoreWorkload[]
|
||||
}
|
||||
|
||||
function Summary ({ workloadInfos }: Props): React.ReactElement {
|
||||
function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { api, apiEndpoint } = useApi();
|
||||
const totalCores = useBrokerStatus('coreCount');
|
||||
const uiHighlight = apiEndpoint?.ui.color || defaultHighlight;
|
||||
const { idles, pools, tasks }: statsType = React.useMemo(() => getStats(totalCores, workloadInfos), [totalCores, workloadInfos]);
|
||||
|
||||
const salesInfo = useBrokerSalesInfo();
|
||||
|
||||
return (
|
||||
<SummaryBox>
|
||||
<StyledSection style={{ display: 'flex' }}>
|
||||
{api.query.broker && (
|
||||
<>
|
||||
<StyledDiv>
|
||||
<CardSummary label={t('current timeslice')}>
|
||||
<CardSummary label={t('timeslice (ts)')}>
|
||||
<Timeslice />
|
||||
</CardSummary>
|
||||
<CardSummary label={t('core count')}>
|
||||
<Cores />
|
||||
</CardSummary>
|
||||
<CardSummary label={t('timeslice period')}>
|
||||
<CardSummary label={t('block per ts')}>
|
||||
<TimeslicePeriod />
|
||||
</CardSummary>
|
||||
<CardSummary label={t('region length')}>
|
||||
<CardSummary label={t('region (ts)')}>
|
||||
<RegionLength />
|
||||
</CardSummary>
|
||||
<CardSummary label={t('estimated bulk price')}>
|
||||
<RenewalPrice />
|
||||
<div className='ui--balance-value'>
|
||||
{formatBalance(salesInfo?.endPrice) || '-'}
|
||||
</div>
|
||||
</CardSummary>
|
||||
<CardSummary label={t('total cores')}>
|
||||
{coreCount}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('cores sold/offered')}>
|
||||
<div>
|
||||
{salesInfo?.coresSold} / {salesInfo?.coresOffered}
|
||||
</div>
|
||||
</CardSummary>
|
||||
</StyledDiv>
|
||||
</>
|
||||
@ -73,8 +83,8 @@ function Summary ({ workloadInfos }: Props): React.ReactElement {
|
||||
<div style={{ marginLeft: '2rem' }}>
|
||||
<UsageBar
|
||||
data={[
|
||||
{ color: '#04AA6D', label: 'Pools', value: idles },
|
||||
{ color: '#FFFFFF', label: 'Idle', value: pools },
|
||||
{ color: '#FFFFFF', label: 'Idle', value: idles },
|
||||
{ color: '#04AA6D', label: 'Pools', value: pools },
|
||||
{ color: uiHighlight, label: 'Tasks', value: tasks }]
|
||||
}
|
||||
></UsageBar>
|
||||
|
||||
@ -39,8 +39,8 @@ function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement {
|
||||
if (!data.task) {
|
||||
return (
|
||||
<>
|
||||
<td style={{ width: 200 }}>no task assigned</td>
|
||||
<td colSpan={5} />
|
||||
<td style={{ width: 200 }}>no task</td>
|
||||
<td colSpan={6} />
|
||||
</>);
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement {
|
||||
header='type'
|
||||
value={'Reservation'}
|
||||
/>
|
||||
<td colSpan={3} />
|
||||
<td colSpan={4} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -94,46 +94,53 @@ function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement {
|
||||
header='type'
|
||||
value={'Legacy Lease'}
|
||||
/>
|
||||
<td colSpan={1} />
|
||||
</>);
|
||||
}
|
||||
|
||||
default: {
|
||||
return <>
|
||||
<TableCol
|
||||
header='Task'
|
||||
value={data.task}
|
||||
/>
|
||||
<TableCol
|
||||
header='Blocks per timeslice'
|
||||
value={data.maskBits}
|
||||
/>
|
||||
<TableCol
|
||||
header='Start'
|
||||
hide='both'
|
||||
value={data.start}
|
||||
/>
|
||||
<TableCol
|
||||
header='End'
|
||||
hide='both'
|
||||
value={data.end}
|
||||
/>
|
||||
<TableCol
|
||||
header='Last block'
|
||||
value={data.endBlock}
|
||||
/>
|
||||
<StyledTableCol hide='mobile'>
|
||||
<h5 style={{ opacity: '0.6' }}>{'Owner'}</h5>
|
||||
{data.owner
|
||||
? (
|
||||
<AddressMini
|
||||
isPadded={false}
|
||||
key={data.owner}
|
||||
value={data.owner}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<TableCol
|
||||
header='Task'
|
||||
value={data.task}
|
||||
/>
|
||||
<TableCol
|
||||
header='Blocks per timeslice'
|
||||
value={data.maskBits}
|
||||
/>
|
||||
<TableCol
|
||||
header='Start'
|
||||
hide='both'
|
||||
value={data.start}
|
||||
/>
|
||||
<TableCol
|
||||
header='End'
|
||||
hide='both'
|
||||
value={data.end}
|
||||
/>
|
||||
<TableCol
|
||||
header='Last block'
|
||||
value={data.endBlock}
|
||||
/>
|
||||
<TableCol
|
||||
header='type'
|
||||
value={'Bulk Coretime'}
|
||||
/>
|
||||
<StyledTableCol hide='mobile'>
|
||||
<h5 style={{ opacity: '0.6' }}>{'Owner'}</h5>
|
||||
{data.owner
|
||||
? (
|
||||
<AddressMini
|
||||
isPadded={false}
|
||||
key={data.owner}
|
||||
value={data.owner}
|
||||
/>
|
||||
)
|
||||
|
||||
: <p> </p>}
|
||||
</StyledTableCol></>;
|
||||
: <p> </p>}
|
||||
</StyledTableCol>
|
||||
</>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,12 +3,14 @@
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { RegionInfo } from '@polkadot/react-hooks/types';
|
||||
import type { Option } from '@polkadot/types';
|
||||
import type { PalletBrokerStatusRecord } from '@polkadot/types/lookup';
|
||||
import type { CoreWorkloadType, CoreWorkplanType, InfoRow } from '../types.js';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ExpandButton } from '@polkadot/react-components';
|
||||
import { useRegions, useToggle } from '@polkadot/react-hooks';
|
||||
import { useApi, useBrokerSalesInfo, useCall, useRegions, useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { formatRowInfo } from '../utils.js';
|
||||
import WorkInfoRow from './WorkInfoRow.js';
|
||||
@ -16,63 +18,90 @@ import Workplan from './Workplan.js';
|
||||
|
||||
interface Props {
|
||||
api: ApiPromise;
|
||||
value: CoreWorkloadType
|
||||
timeslice: number;
|
||||
workplan?: CoreWorkplanType[] | null
|
||||
core: number;
|
||||
workload: CoreWorkloadType[] | undefined
|
||||
workplan?: CoreWorkplanType[] | undefined
|
||||
}
|
||||
|
||||
function Workload ({ api, timeslice, value: { core, info, lastBlock, type }, workplan }: Props): React.ReactElement<Props> {
|
||||
function Workload ({ api, core, workload, workplan }: Props): React.ReactElement<Props> {
|
||||
const { isApiReady } = useApi();
|
||||
const salesInfo = useBrokerSalesInfo();
|
||||
|
||||
const status = useCall<Option<PalletBrokerStatusRecord>>(isApiReady && api.query.broker?.status);
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||
const [tableData, setTableData] = useState<InfoRow>();
|
||||
const [currentRegion, setCurrentRegion] = useState<RegionInfo | undefined>();
|
||||
const [workloadData, setWorkloadData] = useState<InfoRow[]>();
|
||||
const [workplanData, setWorkplanData] = useState<InfoRow[]>();
|
||||
|
||||
const currentTimeSlice = useMemo(() => {
|
||||
if (status?.isSome) {
|
||||
return status.unwrap().lastCommittedTimeslice.toNumber();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}, [status]);
|
||||
|
||||
const regionInfo = useRegions(api);
|
||||
const region: RegionInfo | undefined = useMemo(() => regionInfo?.find((v) => v.core === core && v.start <= currentTimeSlice && v.end > currentTimeSlice), [regionInfo, core, currentTimeSlice]);
|
||||
|
||||
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);
|
||||
if (!!workload?.length && !!salesInfo) {
|
||||
setWorkloadData(formatRowInfo(workload, core, region, currentTimeSlice, salesInfo));
|
||||
} else {
|
||||
return setWorkloadData([{ core }]);
|
||||
}
|
||||
}, [info, regionInfo, core, timeslice, lastBlock, type]);
|
||||
}, [workload, region, currentTimeSlice, core, salesInfo]);
|
||||
|
||||
const hasWorkplan = !!workplan?.length;
|
||||
useEffect(() => {
|
||||
if (!!workplan?.length && !!salesInfo) {
|
||||
setWorkplanData(formatRowInfo(workplan, core, region, currentTimeSlice, salesInfo));
|
||||
}
|
||||
}
|
||||
, [workplan, region, currentTimeSlice, core, salesInfo]);
|
||||
|
||||
const hasWorkplan = workplan?.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
{tableData &&
|
||||
{!!workloadData &&
|
||||
<tr
|
||||
className={`isExpanded isFirst ${isExpanded ? '' : 'isLast'}`}
|
||||
key={tableData.core}
|
||||
key={core}
|
||||
>
|
||||
<WorkInfoRow data={tableData} />
|
||||
<td style={{ paddingRight: '2rem', textAlign: 'right', verticalAlign: 'top' }}>
|
||||
<h5 style={{ opacity: '0.6' }}>Workplan ({workplan?.length})</h5>
|
||||
{hasWorkplan &&
|
||||
(
|
||||
<ExpandButton
|
||||
expanded={isExpanded}
|
||||
onClick={toggleIsExpanded}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{!hasWorkplan && 'none'}
|
||||
</td>
|
||||
{workloadData.map((one) => {
|
||||
return (
|
||||
<React.Fragment key={`${one.endBlock}${one.core}`}>
|
||||
<WorkInfoRow data={one} />
|
||||
<td style={{ paddingRight: '2rem', textAlign: 'right', verticalAlign: 'top' }}>
|
||||
<h5 style={{ opacity: '0.6' }}>Workplan {workplan?.length ? `(${workplan?.length})` : ''}</h5>
|
||||
{!!hasWorkplan &&
|
||||
(
|
||||
<ExpandButton
|
||||
expanded={isExpanded}
|
||||
onClick={toggleIsExpanded}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{!hasWorkplan && 'none'}
|
||||
</td>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
</tr>
|
||||
}
|
||||
{isExpanded &&
|
||||
<>
|
||||
<tr>
|
||||
<td style={{ fontWeight: 700, paddingTop: '2rem', width: 150 }}>workplans</td>
|
||||
<td colSpan={6}></td>
|
||||
<td colSpan={7}></td>
|
||||
</tr>
|
||||
{workplan?.map((workplanInfo) => (
|
||||
{workplanData?.map((workplanInfo) => (
|
||||
<Workplan
|
||||
currentTimeSlice={timeslice}
|
||||
currentTimeSlice={currentTimeSlice}
|
||||
isExpanded={isExpanded}
|
||||
key={workplanInfo.core}
|
||||
region={currentRegion}
|
||||
value={workplanInfo}
|
||||
region={region}
|
||||
workplanData={workplanInfo}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@ -2,29 +2,24 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { RegionInfo } from '@polkadot/react-hooks/types';
|
||||
import type { CoreWorkplanType, InfoRow } from '../types.js';
|
||||
import type { InfoRow } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { Spinner } from '@polkadot/react-components';
|
||||
|
||||
import { formatRowInfo } from '../utils.js';
|
||||
import WorkInfoRow from './WorkInfoRow.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: CoreWorkplanType;
|
||||
workplanData: InfoRow;
|
||||
currentTimeSlice: number
|
||||
isExpanded: boolean
|
||||
region: RegionInfo | undefined
|
||||
}
|
||||
|
||||
function Workplan ({ currentTimeSlice, isExpanded, region, value: { core, info, lastBlock, type } }: Props): React.ReactElement<Props> {
|
||||
const tableData: InfoRow = useMemo(() => {
|
||||
return formatRowInfo(info, core, region, currentTimeSlice, type, lastBlock);
|
||||
}, [info, core, region, currentTimeSlice, type, lastBlock]);
|
||||
|
||||
if (!tableData) {
|
||||
function Workplan ({ isExpanded, workplanData }: Props): React.ReactElement<Props> {
|
||||
if (!workplanData) {
|
||||
return (
|
||||
<tr
|
||||
className={` ${isExpanded ? 'isExpanded isLast' : 'isCollapsed'}`}
|
||||
@ -38,13 +33,12 @@ function Workplan ({ currentTimeSlice, isExpanded, region, value: { core, info,
|
||||
|
||||
return (
|
||||
<>
|
||||
{tableData && (
|
||||
{workplanData && (
|
||||
<tr
|
||||
className={` ${isExpanded ? 'isExpanded isLast' : 'isCollapsed'}`}
|
||||
key={tableData.core}
|
||||
style={{ minHeight: '100px' }}
|
||||
key={workplanData.core}
|
||||
>
|
||||
<WorkInfoRow data={tableData} />
|
||||
<WorkInfoRow data={workplanData} />
|
||||
<td />
|
||||
</tr>
|
||||
)}
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
// 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 type { CoreInfo } from '../types.js';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useApi, useBrokerLeases, useBrokerReservations, useCall, useWorkloadInfos, useWorkplanInfos } from '@polkadot/react-hooks';
|
||||
import { useApi, useBrokerLeases, useBrokerReservations, useBrokerStatus, 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';
|
||||
@ -18,85 +15,84 @@ import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
apiEndpoint?: LinkOption | null;
|
||||
api: ApiPromise;
|
||||
isReady: boolean;
|
||||
}
|
||||
|
||||
const formatLoad = (data: CoreWorkplan[] | CoreWorkload[] | undefined, leaseMap: Record<number, LegacyLease>, reservationMap: Record<number, Reservation>): CoreWorkloadType[] | CoreWorkplanType[] => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
type LeaseMapType = Record<number, LegacyLease>
|
||||
type ReservationMapType = Record<number, Reservation>
|
||||
|
||||
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])
|
||||
const formatDataObject = (one: CoreWorkplan | CoreWorkload, leaseMap: LeaseMapType, reservationMap: ReservationMapType) => ({
|
||||
...one,
|
||||
lastBlock: leaseMap[one?.info.task as number]?.until || 0,
|
||||
maskBits: one.info.maskBits,
|
||||
task: one.info.task,
|
||||
type: getOccupancyType(leaseMap[one.info.task as number], reservationMap[one.info.task as number])
|
||||
});
|
||||
|
||||
const formatData = (coreCount: number, workplan: CoreWorkplan[], workload: CoreWorkload[], leaseMap: LeaseMapType, reservationMap: ReservationMapType): CoreInfo[] => {
|
||||
return Array.from({ length: coreCount }, (_, coreNumber) => {
|
||||
const processWorkload = (data: CoreWorkload[]) => data
|
||||
.filter((load) => load.core === coreNumber)
|
||||
.map((one) => (formatDataObject(one, leaseMap, reservationMap)));
|
||||
|
||||
const processWorkplan = (data: CoreWorkplan[]) => data
|
||||
.filter((load) => load.core === coreNumber)
|
||||
.map((one) => ({
|
||||
...formatDataObject(one, leaseMap, reservationMap),
|
||||
timeslice: one.timeslice
|
||||
}));
|
||||
|
||||
const coreData = {
|
||||
core: coreNumber,
|
||||
workload: workload?.length ? processWorkload(workload) : [],
|
||||
workplan: workplan?.length ? processWorkplan(workplan) : []
|
||||
};
|
||||
|
||||
if ('timeslice' in w) {
|
||||
return result as CoreWorkplanType;
|
||||
} else {
|
||||
return result as CoreWorkloadType;
|
||||
}
|
||||
return coreData;
|
||||
});
|
||||
};
|
||||
|
||||
function Overview ({ api, apiEndpoint, className, isReady }: Props): React.ReactElement<Props> {
|
||||
const [workLoad, setWorkLoad] = useState<CoreWorkloadType[] | undefined>();
|
||||
const [workPlan, setWorkPlan] = useState<CoreWorkplanType[]>();
|
||||
const [filtered, setFiltered] = useState<CoreWorkloadType[]>();
|
||||
const { isApiReady } = useApi();
|
||||
function Overview ({ className }: Props): React.ReactElement<Props> {
|
||||
const { api, apiEndpoint, isApiReady } = useApi();
|
||||
const [data, setData] = useState<CoreInfo[]>([]);
|
||||
|
||||
const [filtered, setFiltered] = useState<CoreInfo[]>();
|
||||
const coreCount = useBrokerStatus('coreCount') || '-';
|
||||
|
||||
const status = useCall<PalletBrokerStatusRecord>(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(() => {
|
||||
if (!!status && !!status?.toHuman()) {
|
||||
const timeslice = status?.toHuman().lastCommittedTimeslice?.toString();
|
||||
|
||||
return timeslice === undefined ? '' : timeslice.toString().split(',').join('');
|
||||
}
|
||||
|
||||
return '0';
|
||||
}, [status]);
|
||||
|
||||
useEffect(() =>
|
||||
setWorkPlan(formatLoad(workplanInfos, leaseMap, reservationMap))
|
||||
, [workplanInfos, leaseMap, reservationMap]);
|
||||
const leaseMap: LeaseMapType = useMemo(() => leases ? createTaskMap(leases) : [], [leases]);
|
||||
const reservationMap: ReservationMapType = useMemo(() => reservations ? createTaskMap(reservations) : [], [reservations]);
|
||||
|
||||
useEffect(() => {
|
||||
setWorkLoad(formatLoad(workloadInfos, leaseMap, reservationMap));
|
||||
}, [workloadInfos, leaseMap, reservationMap]);
|
||||
!!workplanInfos && !!workloadInfos && !!coreCount &&
|
||||
setData(formatData(Number(coreCount), workplanInfos, workloadInfos, leaseMap, reservationMap));
|
||||
}, [workplanInfos, workloadInfos, leaseMap, reservationMap, coreCount]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary
|
||||
apiEndpoint={apiEndpoint}
|
||||
workloadInfos={workLoad}
|
||||
coreCount={coreCount}
|
||||
workloadInfos={workloadInfos}
|
||||
></Summary>
|
||||
{!!workPlan?.length &&
|
||||
{!!data?.length &&
|
||||
(<>
|
||||
<Filters
|
||||
data={data}
|
||||
onFilter={setFiltered}
|
||||
workLoad={workLoad}
|
||||
/>
|
||||
<CoresTable
|
||||
api={api}
|
||||
timeslice={Number(timesliceAsString)}
|
||||
workloadInfos={filtered || workLoad}
|
||||
workplanInfos={workPlan}
|
||||
/>
|
||||
{!!filtered && (
|
||||
<CoresTable
|
||||
api={api}
|
||||
data={filtered}
|
||||
/>
|
||||
)}
|
||||
</>)
|
||||
}
|
||||
{!workPlan?.length && <p style={{ marginLeft: '22px', marginTop: '3rem', opacity: 0.7 }}> No data currently available</p>}
|
||||
{!data?.length && <p style={{ marginLeft: '22px', marginTop: '3rem', opacity: 0.7 }}> No data currently available</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ 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';
|
||||
@ -29,7 +28,6 @@ function createItemsRef (t: (key: string, options?: { replace: Record<string, un
|
||||
function BrokerApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const itemsRef = useRef(createItemsRef(t));
|
||||
const { api, apiEndpoint, isApiReady } = useApi();
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
@ -37,11 +35,7 @@ function BrokerApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
basePath={basePath}
|
||||
items={itemsRef.current}
|
||||
/>
|
||||
<Overview
|
||||
api={api}
|
||||
apiEndpoint={apiEndpoint}
|
||||
isReady={isApiReady}
|
||||
/>
|
||||
<Overview />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
// Copyright 2017-2024 @polkadot/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CoreWorkplan } from '@polkadot/react-hooks/types';
|
||||
import type { CoreWorkload, CoreWorkplan } from '@polkadot/react-hooks/types';
|
||||
|
||||
export interface InfoRow {
|
||||
task: string | number,
|
||||
maskBits: number,
|
||||
task?: string | number,
|
||||
maskBits?: number,
|
||||
core: number
|
||||
mask?: string
|
||||
start?: string | null,
|
||||
@ -31,7 +31,7 @@ export interface statsType {
|
||||
export enum Occupancy {
|
||||
'Reservation',
|
||||
'Lease',
|
||||
'Rent'
|
||||
'Bulk Coretime'
|
||||
}
|
||||
|
||||
export interface CoreWorkplanType extends CoreWorkplan {
|
||||
@ -39,7 +39,7 @@ export interface CoreWorkplanType extends CoreWorkplan {
|
||||
type: Occupancy
|
||||
}
|
||||
|
||||
export interface CoreWorkloadType extends CoreWorkplan {
|
||||
export interface CoreWorkloadType extends CoreWorkload {
|
||||
lastBlock: number,
|
||||
type: Occupancy
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
// 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 type { CoreWorkload, LegacyLease, RegionInfo, Reservation } from '@polkadot/react-hooks/types';
|
||||
import type { CoreWorkloadType, CoreWorkplanType, InfoRow } from './types.js';
|
||||
|
||||
import { BN } from '@polkadot/util';
|
||||
|
||||
import { Occupancy } from './types.js';
|
||||
|
||||
const CoreTimeConsts = {
|
||||
export const CoreTimeConsts = {
|
||||
BlockTime: 6000,
|
||||
BlocksPerTimeslice: 80,
|
||||
DefaultRegion: 5040
|
||||
@ -22,24 +22,36 @@ function formatDate (date: Date) {
|
||||
return `${day} ${month} ${year}`;
|
||||
}
|
||||
|
||||
export const estimateTime = (targetBlock: string, latestBlock: number, timestamp: number): string | null => {
|
||||
if (!timestamp || !latestBlock || !targetBlock) {
|
||||
/**
|
||||
* blockTime = 6000 ms
|
||||
* BlocksPerTimeslice = 80
|
||||
* Default Regoin = 5040 timeslices
|
||||
* TargetBlock = TargetTimeslice * BlocksPerTimeslice
|
||||
* Block Time Difference = |TargetBlock - latest Block| * blockTime
|
||||
*
|
||||
* Estimate timestamp =
|
||||
* if targetBlock is before the latestBlock
|
||||
* now minus block time difference
|
||||
* else
|
||||
* now plus block time difference
|
||||
*/
|
||||
export const estimateTime = (targetTimeslice: string | number, latestBlock: number): string | null => {
|
||||
if (!latestBlock || !targetTimeslice) {
|
||||
console.error('Invalid input: one or more inputs are missing');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
|
||||
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);
|
||||
const timestampBN = new BN(now);
|
||||
const targetBlockBN = new BN(targetTimeslice).mul(timeSlice);
|
||||
const blockTimeDifference = targetBlockBN.sub(latestBlockBN).mul(blockTime);
|
||||
const estTimestamp = timestampBN.add(blockTimeDifference);
|
||||
|
||||
return formatDate(new Date(estTimestamp.toNumber()));
|
||||
} catch (error) {
|
||||
@ -49,43 +61,42 @@ export const estimateTime = (targetBlock: string, latestBlock: number, timestamp
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
export function formatRowInfo (data: CoreWorkloadType[] | CoreWorkplanType[], core: number, currentRegion: RegionInfo | undefined, timeslice: number, { regionBegin, regionEnd }: { regionBegin: number, regionEnd: number }, regionLength = CoreTimeConsts.DefaultRegion): InfoRow[] {
|
||||
return data.map((one: CoreWorkloadType | CoreWorkplanType) => {
|
||||
const item: InfoRow = { core, maskBits: one?.info?.maskBits, task: one?.info?.task, type: one?.type };
|
||||
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.type = one.type;
|
||||
let end; let start = null;
|
||||
|
||||
if (one.type === Occupancy.Lease) {
|
||||
const period = Math.floor(one.lastBlock / regionLength);
|
||||
|
||||
end = period * regionLength;
|
||||
} else {
|
||||
start = currentRegion?.start?.toString() ?? regionBegin;
|
||||
end = currentRegion?.end?.toString() ?? regionEnd;
|
||||
}
|
||||
|
||||
item.owner = currentRegion?.owner.toString();
|
||||
}
|
||||
item.start = start ? estimateTime(start, blockNumber) : null;
|
||||
item.end = end ? estimateTime(end, blockNumber) : null;
|
||||
item.endBlock = end ? Number(end) * 80 : 0;
|
||||
|
||||
item.type = type;
|
||||
if ('timeslice' in one && !start) {
|
||||
start = estimateTime(one.timeslice, blockNumber) ?? null;
|
||||
}
|
||||
|
||||
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;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
export function getStats (totalCores: string | undefined, workloadInfos: CoreWorkloadType[] | CoreWorkloadType | undefined) {
|
||||
export function getStats (totalCores: string | undefined, workloadInfos: CoreWorkload[] | undefined) {
|
||||
if (!totalCores || !workloadInfos) {
|
||||
return { idles: 0, pools: 0, tasks: 0 };
|
||||
}
|
||||
|
||||
const sanitized: CoreWorkloadType[] = Array.isArray(workloadInfos) ? workloadInfos : [workloadInfos];
|
||||
|
||||
const { pools, tasks } = sanitized.reduce(
|
||||
const { pools, tasks } = workloadInfos.reduce(
|
||||
(acc, { info }) => {
|
||||
if (info.isTask) {
|
||||
acc.tasks += 1;
|
||||
@ -111,5 +122,5 @@ export const createTaskMap = <T extends { task: string }>(items: T[]): Record<nu
|
||||
};
|
||||
|
||||
export const getOccupancyType = (lease: LegacyLease | undefined, reservation: Reservation | undefined): Occupancy => {
|
||||
return reservation ? Occupancy.Reservation : lease ? Occupancy.Lease : Occupancy.Rent;
|
||||
return reservation ? Occupancy.Reservation : lease ? Occupancy.Lease : Occupancy['Bulk Coretime'];
|
||||
};
|
||||
|
||||
@ -264,7 +264,8 @@ export interface RegionInfo {
|
||||
|
||||
export interface Reservation {
|
||||
task: string
|
||||
mask: number,
|
||||
mask: string[],
|
||||
maskBits: number
|
||||
}
|
||||
|
||||
export interface LegacyLease {
|
||||
|
||||
@ -24,7 +24,8 @@ function useBrokerReservationsImpl (api: ApiPromise, ready: boolean): Reservatio
|
||||
setState(
|
||||
reservations.map((info: PalletBrokerScheduleItem[]) => {
|
||||
return {
|
||||
mask: processHexMask(info[0]?.mask)?.length ?? 0,
|
||||
mask: processHexMask(info[0]?.mask),
|
||||
maskBits: processHexMask(info[0]?.mask)?.length ?? 0,
|
||||
task: info[0]?.assignment?.isTask ? info[0]?.assignment?.asTask.toString() : info[0]?.assignment?.isPool ? 'Pool' : ''
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user