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:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
@@ -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 || <>&nbsp;</>}</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);
+103
View File
@@ -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);