mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-14 01:41:10 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { BrokerStatus, ChainConstants, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord } from '@pezkuwi/react-hooks/types';
|
||||
import type { CurrentRegion } from './types.js';
|
||||
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
|
||||
import { useBrokerConfig, useBrokerSalesInfo, useBrokerStatus, useCoretimeConsts } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { estimateTime } from './utils.js';
|
||||
|
||||
interface BrokerProviderProps {
|
||||
children: ReactNode;
|
||||
api: ApiPromise;
|
||||
isApiReady: boolean;
|
||||
}
|
||||
|
||||
interface BrokerContextProps {
|
||||
config: PalletBrokerConfigRecord | null,
|
||||
coretimeConsts: ChainConstants | null,
|
||||
currentRegion: CurrentRegion,
|
||||
saleInfo: PalletBrokerSaleInfoRecord | null,
|
||||
status: BrokerStatus | null,
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
config: null,
|
||||
coretimeConsts: null,
|
||||
currentRegion: {
|
||||
begin: null,
|
||||
beginDate: null,
|
||||
end: null,
|
||||
endDate: null
|
||||
},
|
||||
saleInfo: null,
|
||||
status: null
|
||||
};
|
||||
|
||||
export const BrokerContext = createContext<BrokerContextProps>(initialState);
|
||||
|
||||
export const BrokerProvider = ({ api, children, isApiReady }: BrokerProviderProps) => {
|
||||
const coretimeConsts = useCoretimeConsts();
|
||||
const config = useBrokerConfig(api, isApiReady);
|
||||
const saleInfo = useBrokerSalesInfo(api, isApiReady);
|
||||
const status = useBrokerStatus(api, isApiReady);
|
||||
|
||||
const currentRegionEnd = useMemo(() => saleInfo?.regionBegin, [saleInfo]);
|
||||
const currentRegionBegin = useMemo(() => saleInfo && config && saleInfo?.regionBegin - config?.regionLength, [saleInfo, config]);
|
||||
const lastBlock = useMemo(() => status && coretimeConsts && status.lastTimeslice * coretimeConsts.relay.blocksPerTimeslice, [status, coretimeConsts]);
|
||||
|
||||
const currentRegionEndDate = useMemo(() => currentRegionEnd && lastBlock && estimateTime(Number(currentRegionEnd), lastBlock)?.formattedDate, [currentRegionEnd, lastBlock]);
|
||||
const currentRegionBeginDate = useMemo(() => currentRegionBegin && lastBlock && estimateTime(Number(currentRegionBegin), lastBlock)?.formattedDate, [currentRegionBegin, lastBlock]);
|
||||
|
||||
const value = useMemo(() => {
|
||||
if (!config || !saleInfo || !status || !currentRegionBegin || !currentRegionBeginDate || !currentRegionEnd || !currentRegionEndDate || !coretimeConsts) {
|
||||
return initialState;
|
||||
}
|
||||
|
||||
return ({
|
||||
config,
|
||||
coretimeConsts,
|
||||
currentRegion: {
|
||||
begin: currentRegionBegin,
|
||||
beginDate: currentRegionBeginDate,
|
||||
end: currentRegionEnd,
|
||||
endDate: currentRegionEndDate
|
||||
},
|
||||
saleInfo,
|
||||
status
|
||||
});
|
||||
}, [currentRegionBegin, currentRegionEnd, currentRegionBeginDate, currentRegionEndDate, config, saleInfo, status, coretimeConsts]);
|
||||
|
||||
return (
|
||||
<BrokerContext.Provider value={value}>
|
||||
{children}
|
||||
</BrokerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useBrokerContext = () => useContext(BrokerContext);
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { PalletBrokerConfigRecord } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { type CoreWorkloadType, type CoreWorkplanType } from '../types.js';
|
||||
import Workload from './Workload.js';
|
||||
|
||||
interface Props {
|
||||
api: ApiPromise;
|
||||
core: number;
|
||||
config: PalletBrokerConfigRecord,
|
||||
workload?: CoreWorkloadType[],
|
||||
workplan?: CoreWorkplanType[],
|
||||
}
|
||||
|
||||
function CoreTable ({ api, config, 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)?][] = [
|
||||
[
|
||||
<div key={`header${core}`}>{headerRef.current} {core} <span></span></div>,
|
||||
'core',
|
||||
9,
|
||||
undefined
|
||||
]
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
emptySpinner={true}
|
||||
header={header}
|
||||
isSplit={false}
|
||||
key={core}
|
||||
>
|
||||
<Workload
|
||||
api={api}
|
||||
config={config}
|
||||
core={core}
|
||||
key={core}
|
||||
workload={workload}
|
||||
workplan={workplan}
|
||||
/>
|
||||
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CoreTable);
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { CoreInfo } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useBrokerContext } from '../BrokerContext.js';
|
||||
import CoreTable from './CoreTable.js';
|
||||
|
||||
interface Props {
|
||||
api: ApiPromise;
|
||||
isApiReady: boolean;
|
||||
data: CoreInfo[];
|
||||
}
|
||||
|
||||
function CoresTable ({ api, data }: Props): React.ReactElement<Props> {
|
||||
const { config } = useBrokerContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{config && data?.map((coreData) => {
|
||||
return (
|
||||
<CoreTable
|
||||
api={api}
|
||||
config={config}
|
||||
core={coreData?.core}
|
||||
key={coreData?.core}
|
||||
workload={coreData?.workload}
|
||||
workplan={coreData?.workplan}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CoresTable);
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright 2017-2025 @pezkuwi/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 '@pezkuwi/react-components';
|
||||
import { useDebounce } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
@media (max-width: 768px) {
|
||||
max-width: 100%:
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
data: CoreInfo[];
|
||||
onFilter: (data: CoreInfo[]) => void
|
||||
}
|
||||
|
||||
const filterLoad = (teyrchainId: string, data: CoreInfo[], workloadCoreSelected: number): CoreInfo[] => {
|
||||
if (teyrchainId) {
|
||||
return data.filter(({ workload, workplan }) => !!workload?.filter(({ info }) => info.task === teyrchainId).length || !!workplan?.filter(({ info }) => info.task === teyrchainId).length);
|
||||
}
|
||||
|
||||
if (workloadCoreSelected === -1) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return data.filter((one) => one.core === workloadCoreSelected);
|
||||
};
|
||||
|
||||
function Filters ({ data, onFilter }: Props): React.ReactElement<Props> {
|
||||
const [workloadCoreSelected, setWorkloadCoreSelected] = useState(-1);
|
||||
const [_teyrchainId, setTeyrchainId] = useState<string>('');
|
||||
|
||||
const coreArr: number[] = useMemo(() =>
|
||||
data?.length
|
||||
? Array.from({ length: data.length || 0 }, (_, index) => index)
|
||||
: []
|
||||
, [data]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const teyrchainId = useDebounce(_teyrchainId);
|
||||
|
||||
const workloadCoreOpts = useMemo(
|
||||
() => coreArr && [{ text: t('All active/available cores'), value: -1 }].concat(
|
||||
coreArr
|
||||
.map((c) => (
|
||||
{
|
||||
text: `Core ${c}`,
|
||||
value: c
|
||||
}
|
||||
))
|
||||
.filter((v): v is { text: string, value: number } => !!v.text)
|
||||
),
|
||||
[coreArr, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = filterLoad(teyrchainId, data, workloadCoreSelected);
|
||||
|
||||
onFilter(filtered);
|
||||
}, [data, workloadCoreSelected, teyrchainId, onFilter]);
|
||||
|
||||
const onDropDownChange = useCallback((v: number) => {
|
||||
setWorkloadCoreSelected(v);
|
||||
setTeyrchainId('');
|
||||
}, []);
|
||||
|
||||
const onInputChange = useCallback((v: string) => {
|
||||
setTeyrchainId(v);
|
||||
setWorkloadCoreSelected(-1);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledDiv style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginBottom: '1.5rem', maxWidth: '300px' }}>
|
||||
<Dropdown
|
||||
className='isSmall'
|
||||
label={t('selected core')}
|
||||
onChange={onDropDownChange}
|
||||
options={workloadCoreOpts}
|
||||
value={workloadCoreSelected}
|
||||
/>
|
||||
<div style={{ minWidth: '150px' }}>
|
||||
<Input
|
||||
className='full isSmall'
|
||||
label={t('teyrchain id')}
|
||||
onChange={onInputChange}
|
||||
placeholder={t('teyrchain id')}
|
||||
value={_teyrchainId}
|
||||
/>
|
||||
</div>
|
||||
</StyledDiv>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Filters);
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
|
||||
import type { statsType } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, styled, SummaryBox, UsageBar } from '@pezkuwi/react-components';
|
||||
import { defaultHighlight } from '@pezkuwi/react-components/styles';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
import { type CoreWorkload } from '@pezkuwi/react-hooks/types';
|
||||
import { BN, BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
import { useBrokerContext } from '../BrokerContext.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { getStats } from '../utils.js';
|
||||
import RegionLength from './Summary/RegionLength.js';
|
||||
import Timeslice from './Summary/Timeslice.js';
|
||||
import TimeslicePeriod from './Summary/TimeslicePeriod.js';
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
const StyledSection = styled.section`
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
apiEndpoint?: LinkOption | null;
|
||||
coreCount?: string
|
||||
workloadInfos?: CoreWorkload[]
|
||||
}
|
||||
|
||||
function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { api, apiEndpoint } = useApi();
|
||||
const uiHighlight = apiEndpoint?.ui.color || defaultHighlight;
|
||||
const { idles, pools, tasks }: statsType = React.useMemo(() => getStats(coreCount, workloadInfos), [coreCount, workloadInfos]);
|
||||
const { config, currentRegion, saleInfo, status } = useBrokerContext();
|
||||
|
||||
return (
|
||||
<SummaryBox>
|
||||
<StyledSection style={{ display: 'flex' }}>
|
||||
{api.query.broker && (
|
||||
<>
|
||||
<StyledDiv>
|
||||
<CardSummary label={t('timeslice (ts)')}>
|
||||
<Timeslice />
|
||||
</CardSummary>
|
||||
<CardSummary label={t('block per ts')}>
|
||||
<TimeslicePeriod />
|
||||
</CardSummary>
|
||||
<CardSummary label={t('region (ts)')}>
|
||||
<RegionLength />
|
||||
</CardSummary>
|
||||
<CardSummary label={t('total cores')}>
|
||||
{coreCount}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('cores sold/offered')}>
|
||||
<div>
|
||||
{saleInfo?.coresSold} / {saleInfo?.coresOffered}
|
||||
</div>
|
||||
</CardSummary>
|
||||
<CardSummary
|
||||
label={t('cycle progress')}
|
||||
progress={{
|
||||
isBlurred: false,
|
||||
total: new BN(config?.regionLength || 0),
|
||||
value: (config?.regionLength && currentRegion.end && status && new BN(config?.regionLength - (currentRegion.end - status?.lastTimeslice))) || BN_ZERO,
|
||||
withTime: false
|
||||
}}
|
||||
/>
|
||||
</StyledDiv>
|
||||
</>
|
||||
|
||||
)}
|
||||
<div
|
||||
className='media--1400'
|
||||
style={{ marginLeft: '2rem' }}
|
||||
>
|
||||
<UsageBar
|
||||
data={[
|
||||
{ color: '#FFFFFF', label: 'Idle', value: idles },
|
||||
{ color: '#04AA6D', label: 'Pools', value: pools },
|
||||
{ color: uiHighlight, label: 'Tasks', value: tasks }]
|
||||
}
|
||||
></UsageBar>
|
||||
|
||||
</div>
|
||||
</StyledSection>
|
||||
<section>
|
||||
{currentRegion.begin && currentRegion.end &&
|
||||
(
|
||||
<>
|
||||
<CardSummary
|
||||
className='media--1200'
|
||||
label={t('sale dates')}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px' }}>{currentRegion.beginDate}</div>
|
||||
<div style={{ fontSize: '14px' }}>{currentRegion.endDate}</div>
|
||||
</div>
|
||||
</CardSummary>
|
||||
<CardSummary
|
||||
className='media--1200'
|
||||
label={t('sale ts')}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px' }}>{currentRegion.begin}</div>
|
||||
<div style={{ fontSize: '14px' }}>{currentRegion.end}</div>
|
||||
</div>
|
||||
</CardSummary>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletBrokerConfigRecord } from '@pezkuwi/types/lookup';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function RegionLength ({ children, className }: Props): React.ReactElement<Props> | null {
|
||||
const { api } = useApi();
|
||||
const config = useCall<PalletBrokerConfigRecord>(api.query.broker?.configuration);
|
||||
const length = config?.toJSON()?.regionLength;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{length?.toString() || '-'}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(RegionLength);
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useApi, useBrokerStatus } from '@pezkuwi/react-hooks';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Timeslice ({ children, className }: Props): React.ReactElement<Props> | null {
|
||||
const { api, isApiReady } = useApi();
|
||||
const info = useBrokerStatus(api, isApiReady);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{info?.lastTimeslice || '-'}
|
||||
{children}
|
||||
</div>);
|
||||
}
|
||||
|
||||
export default React.memo(Timeslice);
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { u32 } from '@pezkuwi/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function BrokerId ({ children, className }: Props): React.ReactElement<Props> | null {
|
||||
const { api } = useApi();
|
||||
const period = api.consts.broker?.timeslicePeriod as u32;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{period?.toString()}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BrokerId);
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { FlagColor } from '@pezkuwi/react-components/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressMini, styled, Tag } from '@pezkuwi/react-components';
|
||||
import { CoreTimeTypes } from '@pezkuwi/react-hooks/constants';
|
||||
|
||||
import { type InfoRow } from '../types.js';
|
||||
|
||||
const colours: Record<string, string> = {
|
||||
[CoreTimeTypes.Reservation]: 'orange',
|
||||
[CoreTimeTypes.Lease]: 'blue',
|
||||
[CoreTimeTypes['Bulk Coretime']]: 'pink',
|
||||
[CoreTimeTypes['On Demand']]: 'green'
|
||||
};
|
||||
|
||||
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';
|
||||
}) => (
|
||||
<StyledTableCol hide={hide}>
|
||||
<h5 style={{ opacity: '0.6' }}>{header}</h5>
|
||||
<p>{value || <> </>}</p>
|
||||
</StyledTableCol>
|
||||
);
|
||||
|
||||
function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement {
|
||||
if (!data.task) {
|
||||
return (
|
||||
<>
|
||||
<td style={{ width: 200 }}>no task</td>
|
||||
<td colSpan={7} />
|
||||
</>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableCol
|
||||
header='Task'
|
||||
value={data.task}
|
||||
/>
|
||||
<TableCol
|
||||
header='Blocks per timeslice'
|
||||
value={data.maskBits}
|
||||
/>
|
||||
<TableCol
|
||||
header='Start ts'
|
||||
hide='both'
|
||||
value={data.startTimeslice}
|
||||
/>
|
||||
<TableCol
|
||||
header='Start date'
|
||||
hide='both'
|
||||
value={data.start}
|
||||
/>
|
||||
<TableCol
|
||||
header='End date'
|
||||
hide='both'
|
||||
value={data.end}
|
||||
/>
|
||||
<TableCol
|
||||
header='Last block (relay)'
|
||||
value={data.endBlock}
|
||||
/>
|
||||
<StyledTableCol hide={'mobile'}>
|
||||
<h5 style={{ opacity: '0.6' }}>type</h5>
|
||||
{typeof data.type === 'number' && data.type in CoreTimeTypes && (
|
||||
<Tag
|
||||
color={colours[data.type] as FlagColor}
|
||||
label={Object.values(CoreTimeTypes)[data.type]}
|
||||
/>
|
||||
)}
|
||||
</StyledTableCol>
|
||||
{data.owner
|
||||
? <StyledTableCol hide='mobile'>
|
||||
<h5 style={{ opacity: '0.6' }}>{'Owner'}</h5>
|
||||
<AddressMini
|
||||
isPadded={false}
|
||||
key={data.owner}
|
||||
value={data.owner}
|
||||
/>
|
||||
|
||||
</StyledTableCol>
|
||||
: <td></td>}
|
||||
</>);
|
||||
}
|
||||
|
||||
export default React.memo(WorkInfoRow);
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { PalletBrokerConfigRecord, RegionInfo } from '@pezkuwi/react-hooks/types';
|
||||
import type { CoreWorkloadType, CoreWorkplanType, InfoRow } from '../types.js';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ExpandButton } from '@pezkuwi/react-components';
|
||||
import { useRegions, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { useCoretimeConsts } from '@pezkuwi/react-hooks/useCoretimeConsts';
|
||||
|
||||
import { useBrokerContext } from '../BrokerContext.js';
|
||||
import { estimateTime, formatRowInfo } from '../utils.js';
|
||||
import WorkInfoRow from './WorkInfoRow.js';
|
||||
import Workplan from './Workplan.js';
|
||||
|
||||
interface Props {
|
||||
api: ApiPromise;
|
||||
core: number;
|
||||
workload: CoreWorkloadType[] | undefined
|
||||
workplan?: CoreWorkplanType[] | undefined
|
||||
config: PalletBrokerConfigRecord
|
||||
}
|
||||
|
||||
function Workload ({ api, config, core, workload, workplan }: Props): React.ReactElement<Props> {
|
||||
const coretimeConstants = useCoretimeConsts();
|
||||
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||
const [workloadData, setWorkloadData] = useState<InfoRow[]>();
|
||||
const [workplanData, setWorkplanData] = useState<InfoRow[]>();
|
||||
|
||||
const { currentRegion, status } = useBrokerContext();
|
||||
|
||||
const currentTimeSlice = useMemo(() =>
|
||||
status?.lastTimeslice ?? 0
|
||||
, [status]);
|
||||
|
||||
const regionInfo = useRegions(api);
|
||||
const regionOwnerInfo: RegionInfo | undefined = useMemo(() => regionInfo?.find((v) => v.core === core && v.start <= currentTimeSlice && v.end > currentTimeSlice), [regionInfo, core, currentTimeSlice]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!workload?.length && currentTimeSlice > 0) {
|
||||
// saleInfo points to a regionEnd and regionBeing in the next cycle, but we want the start and end of the current cycle
|
||||
setWorkloadData(formatRowInfo(
|
||||
workload,
|
||||
core,
|
||||
regionOwnerInfo,
|
||||
currentTimeSlice,
|
||||
{
|
||||
begin: currentRegion.begin || 0,
|
||||
beginDate: currentRegion.beginDate || '',
|
||||
end: currentRegion.end || 0,
|
||||
endDate: currentRegion.endDate || ''
|
||||
},
|
||||
config.regionLength,
|
||||
coretimeConstants?.relay
|
||||
));
|
||||
} else {
|
||||
return setWorkloadData([{ core }]);
|
||||
}
|
||||
}, [workload, regionOwnerInfo, currentTimeSlice, core, config, coretimeConstants, currentRegion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workplan?.length && status && coretimeConstants && currentRegion.endDate) {
|
||||
const futureRegionStart = currentRegion.end || 0;
|
||||
const futureRegionEnd = futureRegionStart + config.regionLength;
|
||||
const lastBlock = status.lastTimeslice * coretimeConstants?.relay.blocksPerTimeslice;
|
||||
|
||||
setWorkplanData(formatRowInfo(
|
||||
workplan,
|
||||
core,
|
||||
regionOwnerInfo,
|
||||
status.lastTimeslice,
|
||||
{
|
||||
begin: futureRegionStart,
|
||||
beginDate: currentRegion.endDate,
|
||||
end: futureRegionEnd,
|
||||
endDate: estimateTime(futureRegionEnd, lastBlock)?.formattedDate ?? ''
|
||||
},
|
||||
config.regionLength,
|
||||
coretimeConstants?.relay
|
||||
));
|
||||
}
|
||||
}, [workplan, regionOwnerInfo, status, core, config, coretimeConstants, currentRegion]);
|
||||
|
||||
const hasWorkplan = workplan?.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!workloadData &&
|
||||
<tr
|
||||
className={`isExpanded isFirst ${isExpanded ? '' : 'isLast'}`}
|
||||
key={core}
|
||||
>
|
||||
{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={8}></td>
|
||||
</tr>
|
||||
{workplanData?.map((workplanInfo) => (
|
||||
<Workplan
|
||||
isExpanded={isExpanded}
|
||||
key={workplanInfo.core}
|
||||
workplanData={workplanInfo}
|
||||
/>
|
||||
))}
|
||||
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Workload);
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { InfoRow } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Spinner } from '@pezkuwi/react-components';
|
||||
|
||||
import WorkInfoRow from './WorkInfoRow.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
workplanData: InfoRow;
|
||||
isExpanded: boolean
|
||||
}
|
||||
|
||||
function Workplan ({ isExpanded, workplanData }: Props): React.ReactElement<Props> {
|
||||
if (!workplanData) {
|
||||
return (
|
||||
<tr
|
||||
className={` ${isExpanded ? 'isExpanded isLast' : 'isCollapsed'}`}
|
||||
style={{ minHeight: '100px' }}
|
||||
>
|
||||
<td> <Spinner /> </td>
|
||||
<td colSpan={7}></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{workplanData && (
|
||||
<tr
|
||||
className={` ${isExpanded ? 'isExpanded isLast' : 'isCollapsed'}`}
|
||||
key={workplanData.core}
|
||||
>
|
||||
<WorkInfoRow data={workplanData} />
|
||||
<td />
|
||||
</tr>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Workplan);
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CoreWorkload, CoreWorkplan, LegacyLease, Reservation } from '@pezkuwi/react-hooks/types';
|
||||
import type { CoreInfo } from '../types.js';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useApi, useBrokerLeases, useBrokerReservations, useWorkloadInfos, useWorkplanInfos } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useBrokerContext } from '../BrokerContext.js';
|
||||
import { createTaskMap, getOccupancyType } from '../utils.js';
|
||||
import CoresTable from './CoresTables.js';
|
||||
import Filters from './Filters.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type LeaseMapType = Record<number, LegacyLease>
|
||||
type ReservationMapType = Record<number, Reservation>
|
||||
|
||||
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], one.info.isPool)
|
||||
});
|
||||
|
||||
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) : []
|
||||
};
|
||||
|
||||
return coreData;
|
||||
});
|
||||
};
|
||||
|
||||
function Overview ({ className }: Props): React.ReactElement<Props> {
|
||||
const { api, apiEndpoint, isApiReady } = useApi();
|
||||
const { currentRegion, status } = useBrokerContext();
|
||||
|
||||
const [data, setData] = useState<CoreInfo[]>([]);
|
||||
const [filtered, setFiltered] = useState<CoreInfo[]>();
|
||||
|
||||
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: LeaseMapType = useMemo(() => leases ? createTaskMap(leases) : [], [leases]);
|
||||
const reservationMap: ReservationMapType = useMemo(() => reservations ? createTaskMap(reservations) : [], [reservations]);
|
||||
|
||||
useEffect(() => {
|
||||
!!workplanInfos && !!workloadInfos && !!status?.coreCount &&
|
||||
setData(formatData(Number(status?.coreCount), workplanInfos, workloadInfos, leaseMap, reservationMap));
|
||||
}, [workplanInfos, workloadInfos, leaseMap, reservationMap, status]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{!!currentRegion.beginDate && !!currentRegion.endDate &&
|
||||
<Summary
|
||||
apiEndpoint={apiEndpoint}
|
||||
coreCount={status?.coreCount.toString() || '-'}
|
||||
workloadInfos={workloadInfos}
|
||||
/>}
|
||||
{!!data?.length &&
|
||||
(<>
|
||||
<Filters
|
||||
data={data}
|
||||
onFilter={setFiltered}
|
||||
/>
|
||||
{!!filtered && (
|
||||
<CoresTable
|
||||
api={api}
|
||||
data={filtered}
|
||||
isApiReady={isApiReady}
|
||||
/>
|
||||
)}
|
||||
</>)
|
||||
}
|
||||
{!data?.length && <p style={{ marginLeft: '22px', marginTop: '3rem', opacity: 0.7 }}> No data currently available</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Overview);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { TabItem } from '@pezkuwi/react-components/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Tabs } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Overview from './Overview/index.js';
|
||||
import { BrokerProvider } from './BrokerContext.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function createItemsRef (t: (key: string, options?: { replace: Record<string, unknown> }) => string): TabItem[] {
|
||||
return [
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Overview')
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function BrokerApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const itemsRef = useRef(createItemsRef(t));
|
||||
const { api, isApiReady } = useApi();
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
<BrokerProvider
|
||||
api={api}
|
||||
isApiReady={isApiReady}
|
||||
>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
items={itemsRef.current}
|
||||
/>
|
||||
<Overview />
|
||||
</BrokerProvider>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BrokerApp);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/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, unknown> }) => string } {
|
||||
return useTranslationBase('app-broker');
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CoreTimeTypes } from '@pezkuwi/react-hooks/constants';
|
||||
import type { CoreWorkload, CoreWorkplan } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
export interface InfoRow {
|
||||
task?: string | number,
|
||||
maskBits?: number,
|
||||
core: number
|
||||
mask?: string
|
||||
start?: string | null,
|
||||
startTimeslice?: number | null
|
||||
end?: string | null
|
||||
owner?: string
|
||||
leaseLength?: number
|
||||
endBlock?: number
|
||||
type?: CoreTimeTypes
|
||||
}
|
||||
|
||||
export interface CoreInfo {
|
||||
core: number,
|
||||
workload: CoreWorkloadType[] | undefined,
|
||||
workplan: CoreWorkplanType[] | undefined
|
||||
}
|
||||
|
||||
export interface statsType {
|
||||
idles: number,
|
||||
pools: number,
|
||||
tasks: number
|
||||
}
|
||||
|
||||
export interface CoreWorkplanType extends CoreWorkplan {
|
||||
lastBlock: number,
|
||||
type: CoreTimeTypes
|
||||
}
|
||||
|
||||
export interface CoreWorkloadType extends CoreWorkload {
|
||||
lastBlock: number,
|
||||
type: CoreTimeTypes
|
||||
}
|
||||
|
||||
export interface CurrentRegion {
|
||||
begin: number | null,
|
||||
beginDate: string | null,
|
||||
end: number | null,
|
||||
endDate: string | null
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-broker authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ChainBlockConstants, CoreWorkload, LegacyLease, RegionInfo, Reservation } from '@pezkuwi/react-hooks/types';
|
||||
import type { CoreWorkloadType, CoreWorkplanType, InfoRow } from './types.js';
|
||||
|
||||
import { CoreTimeTypes } from '@pezkuwi/react-hooks/constants';
|
||||
import { BN } from '@pezkuwi/util';
|
||||
|
||||
function formatDate (date: Date) {
|
||||
const day = date.getDate();
|
||||
const month = date.toLocaleString('default', { month: 'short' });
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day} ${month} ${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculation on the Relay chain
|
||||
*
|
||||
* blockTime = 6000 ms
|
||||
* BlocksPerTimeslice = 80
|
||||
* Default Region = 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,
|
||||
{ blocksPerTimeslice: blocksPerTs, blocktimeMs }: ChainBlockConstants = { blocksPerTimeslice: 80, blocktimeMs: 6000 }
|
||||
): { timestamp: number, formattedDate: string } | null => {
|
||||
if (!latestBlock || !targetTimeslice) {
|
||||
console.error('Invalid input: one or more inputs are missing');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new BN(Date.now());
|
||||
|
||||
try {
|
||||
const blockTime = new BN(blocktimeMs); // Average block time in milliseconds (6 seconds)
|
||||
const blocksPerTimeslice = new BN(blocksPerTs);
|
||||
const targetBlock = new BN(Number(targetTimeslice)).mul(blocksPerTimeslice);
|
||||
const latestBlockBN = new BN(latestBlock);
|
||||
const blockDifference = targetBlock.sub(latestBlockBN);
|
||||
const timeDifference = blockDifference.mul(blockTime);
|
||||
const estTimestamp = now.add(timeDifference);
|
||||
|
||||
return {
|
||||
formattedDate: formatDate(new Date(estTimestamp.toNumber())),
|
||||
timestamp: estTimestamp.toNumber()
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in calculation:', error);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data: CoreWorkloadType[]
|
||||
* @param core: core number
|
||||
* @param currentRegion
|
||||
* @param timeslice
|
||||
* @param param4
|
||||
* @param regionLength
|
||||
*/
|
||||
export function formatRowInfo (
|
||||
data: CoreWorkloadType[] | CoreWorkplanType[],
|
||||
core: number,
|
||||
regionOwnerInfo: RegionInfo | undefined,
|
||||
currentTimeSlice: number,
|
||||
currentRegion: { begin: number, beginDate: string, end: number, endDate: string },
|
||||
regionLength: number,
|
||||
coretimeRelayConstants: ChainBlockConstants = { blocksPerTimeslice: 0, blocktimeMs: 0 }
|
||||
): InfoRow[] {
|
||||
const blockNumberNow = currentTimeSlice * coretimeRelayConstants.blocksPerTimeslice;
|
||||
|
||||
return data.map((one: CoreWorkloadType | CoreWorkplanType) => {
|
||||
const item: InfoRow = {
|
||||
core,
|
||||
maskBits: one?.info?.maskBits,
|
||||
task: one?.info?.task,
|
||||
type: one?.type
|
||||
};
|
||||
|
||||
// For region-based types, use the provided dates
|
||||
if ([CoreTimeTypes['Bulk Coretime'], CoreTimeTypes.Reservation, CoreTimeTypes['On Demand']].includes(one.type)) {
|
||||
item.start = currentRegion.beginDate;
|
||||
item.end = currentRegion.endDate;
|
||||
item.startTimeslice = currentRegion.begin;
|
||||
item.endBlock = currentRegion.end * coretimeRelayConstants.blocksPerTimeslice;
|
||||
} else if (one.type === CoreTimeTypes.Lease) { // For lease type, calculate the end
|
||||
const period = Math.floor(one.lastBlock / regionLength);
|
||||
const endTs = period * regionLength;
|
||||
const endEstimate = estimateTime(endTs, blockNumberNow, coretimeRelayConstants);
|
||||
|
||||
item.end = endEstimate?.formattedDate ?? null;
|
||||
item.endBlock = endTs * coretimeRelayConstants.blocksPerTimeslice;
|
||||
}
|
||||
|
||||
item.owner = regionOwnerInfo?.owner.toString();
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
export function getStats (totalCores: string | undefined, workloadInfos: CoreWorkload[] | undefined) {
|
||||
if (!totalCores || !workloadInfos) {
|
||||
return { idles: 0, pools: 0, tasks: 0 };
|
||||
}
|
||||
|
||||
const { pools, tasks } = workloadInfos.reduce(
|
||||
(acc, { info }) => {
|
||||
if (info.isTask) {
|
||||
acc.tasks += 1;
|
||||
} else if (info.isPool) {
|
||||
acc.pools += 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ pools: 0, tasks: 0 }
|
||||
);
|
||||
const idles = Number(totalCores) - (pools + tasks);
|
||||
|
||||
return { idles, pools, tasks };
|
||||
}
|
||||
|
||||
export const createTaskMap = <T extends { task: string }>(items: T[]): Record<number, T> => {
|
||||
return (items || []).reduce((acc, item) => {
|
||||
acc[Number(item.task)] = item;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<number, T>);
|
||||
};
|
||||
|
||||
export const getOccupancyType = (lease: LegacyLease | undefined, reservation: Reservation | undefined, isPool: boolean): CoreTimeTypes => {
|
||||
if (isPool) {
|
||||
return CoreTimeTypes['On Demand'];
|
||||
}
|
||||
|
||||
return reservation ? CoreTimeTypes.Reservation : lease ? CoreTimeTypes.Lease : CoreTimeTypes['Bulk Coretime'];
|
||||
};
|
||||
Reference in New Issue
Block a user