Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
2fbdc84
feat(tangle-cloud): Implement service approval flow with security com…
vutuanlinh2k2 Feb 2, 2026
2575ba8
fix(tangle-cloud): UI/UX enhancements for service deployment forms
vutuanlinh2k2 Feb 2, 2026
e543f49
feat(tangle-cloud): add time unit selector for instance duration (TTL)
vutuanlinh2k2 Feb 2, 2026
31f7f6f
feat(tangle-cloud): display tokens at risk in service approval flow
vutuanlinh2k2 Feb 2, 2026
cafaab9
fix(tangle-cloud): convert instance duration to seconds before servic…
vutuanlinh2k2 Feb 2, 2026
cc802b2
fix(tangle-cloud): fix service detail page for service ID 0 and opera…
vutuanlinh2k2 Feb 2, 2026
f354c3d
fix(tangle-cloud): update TTL time units to seconds, minutes, hours, …
vutuanlinh2k2 Feb 2, 2026
7466c59
refactor(tangle-shared-ui): format useContractWrite and useOperatorDe…
vutuanlinh2k2 Feb 4, 2026
7f29259
feat(tangle-shared-ui): add service request hooks and extend GraphQL …
vutuanlinh2k2 Feb 4, 2026
5fd63b1
feat(tangle-cloud): add ServiceRequestDetails components
vutuanlinh2k2 Feb 4, 2026
4bf74fb
feat(tangle-cloud): add service request types
vutuanlinh2k2 Feb 4, 2026
96067ff
refactor(tangle-cloud): replace approval modals with unified service …
vutuanlinh2k2 Feb 4, 2026
d1f744d
feat(tangle-cloud): auto-fetch blueprint config for deployment workflow
vutuanlinh2k2 Feb 4, 2026
bc3b5d8
fix(tangle-dapp): update claim button text to "Claim All Rewards"
vutuanlinh2k2 Feb 4, 2026
b7456b1
Merge branch 'v2' into linh/qa/manage-service-req
vutuanlinh2k2 Feb 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,5 @@ contracts/**/target/
# Contracts - Generated migration data (large files)
**/migration-proofs.json
contracts/**/evm-claims.json

docs/cloud-qa
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { FC } from 'react';
import { Typography } from '@tangle-network/ui-components/typography/Typography';
import { EMPTY_VALUE_PLACEHOLDER } from '@tangle-network/ui-components/constants';
import { isEthereumAddress } from '@polkadot/util-crypto';
import { shortenHex } from '@tangle-network/ui-components/utils/shortenHex';
import { shortenString } from '@tangle-network/ui-components/utils/shortenString';
import { isSubstrateAddress } from '@tangle-network/ui-components/utils/isSubstrateAddress';
import { twMerge } from 'tailwind-merge';

type Props = {
name: string;
author: string;
description: string;
instancesCount: number;
operatorsCount: number;
};

const BlueprintInfoCard: FC<Props> = ({
name,
author,
description,
instancesCount,
operatorsCount,
}) => {
const formattedAuthor = isEthereumAddress(author)
? shortenHex(author)
: isSubstrateAddress(author)
? shortenString(author)
: author;

return (
<div
className={twMerge(
'rounded-xl overflow-hidden',
'border border-mono-0 dark:border-mono-170',
)}
>
<div
className={twMerge(
'relative flex flex-col gap-4 py-4 px-6',
'bg-[linear-gradient(180deg,rgba(184,196,255,0.20)0%,rgba(236,239,255,0.20)100%),linear-gradient(180deg,rgba(255,255,255,0.50)0%,rgba(255,255,255,0.30)100%)]',
'dark:bg-[linear-gradient(180deg,rgba(17,22,50,0.20)0%,rgba(21,37,117,0.20)100%),linear-gradient(180deg,rgba(43,47,64,0.50)0%,rgba(43,47,64,0.30)100%)]',
'before:absolute before:inset-0 before:bg-cover before:bg-no-repeat before:opacity-50 before:pointer-events-none',
"before:bg-[url('/static/assets/blueprints/grid-bg.png')] dark:before:bg-[url('/static/assets/blueprints/grid-bg-dark.png')]",
)}
>
<div className="relative space-y-1">
<Typography variant="h4" className="text-mono-180 dark:text-mono-20">
{name}
</Typography>

<Typography
variant="body1"
className="text-mono-120 dark:text-mono-100"
>
{formattedAuthor}
</Typography>
</div>

<Typography
variant="body2"
className="relative text-mono-200 dark:text-mono-0"
>
{description}
</Typography>

<div className="relative flex w-full gap-1 pt-4">
<div className="flex-1 space-y-2">
<Typography
variant="body2"
className="text-mono-120 dark:text-mono-100"
>
Instances
</Typography>

<Typography variant="h5">
{instancesCount ?? EMPTY_VALUE_PLACEHOLDER}
</Typography>
</div>

<div className="flex-1 space-y-2">
<Typography
variant="body2"
className="text-mono-120 dark:text-mono-100"
>
Operators
</Typography>

<Typography variant="h5">
{operatorsCount ?? EMPTY_VALUE_PLACEHOLDER}
</Typography>
</div>
</div>
</div>
</div>
);
};

export default BlueprintInfoCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { FC } from 'react';
import {
Chip,
SkeletonLoader,
Typography,
} from '@tangle-network/ui-components';
import {
MembershipModel,
getMembershipLabel,
formatTtl,
formatCreatedAt,
} from '../../types/serviceRequest';

type Props = {
ttl: bigint | undefined;
createdAt: bigint | undefined;
membership: MembershipModel | undefined;
minOperators: number | undefined;
maxOperators: number | undefined;
totalOperators: number;
isLoading: boolean;
};

const CommitmentSection: FC<Props> = ({
ttl,
createdAt,
membership,
minOperators,
maxOperators: _maxOperators,
totalOperators,
isLoading,
}) => {
if (isLoading) {
return (
<div className="space-y-2">
<Typography variant="h5" className="text-mono-200 dark:text-mono-0">
Commitment Details
</Typography>

<div className="space-y-1">
<SkeletonLoader className="h-5 w-36" />
<SkeletonLoader className="h-5 w-32" />
<SkeletonLoader className="h-5 w-28" />
</div>
</div>
);
}

const durationText = ttl !== undefined ? formatTtl(ttl) : '-';
const createdText =
createdAt !== undefined ? formatCreatedAt(createdAt) : '-';

// For Fixed: all operators required. For Dynamic: use minOperators.
const minApprovalsRequired =
membership === MembershipModel.Fixed
? totalOperators
: (minOperators ?? totalOperators);

return (
<div className="space-y-2">
<Typography variant="h5" className="text-mono-200 dark:text-mono-0">
Commitment Details
</Typography>

<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm text-mono-140 dark:text-mono-80">
Duration:
</span>
<span className="text-sm font-semibold">{durationText}</span>
</div>

<div className="flex items-center gap-2">
<span className="text-sm text-mono-140 dark:text-mono-80">
Created:
</span>
<span className="text-sm font-semibold">{createdText}</span>
</div>

<div className="flex items-center gap-2">
<span className="text-sm text-mono-140 dark:text-mono-80">
Membership:
</span>
<Chip
color={membership === MembershipModel.Fixed ? 'blue' : 'purple'}
>
{membership !== undefined ? getMembershipLabel(membership) : '-'}
</Chip>
</div>

<div className="flex items-center gap-2">
<span className="text-sm text-mono-140 dark:text-mono-80">
Min. Approvals Required:
</span>
<span className="text-sm font-semibold">{minApprovalsRequired}</span>
</div>
</div>
</div>
);
};

export default CommitmentSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { FC, useMemo } from 'react';
import { Address } from 'viem';
import {
Avatar,
Chip,
SkeletonLoader,
Typography,
} from '@tangle-network/ui-components';
import { shortenString } from '@tangle-network/ui-components/utils/shortenString';
import {
OperatorApprovalStatus,
OperatorWithStatus,
} from '../../types/serviceRequest';

type Props = {
operatorCandidates: Address[];
approvedOperators: Address[];
rejectedOperators: Address[];
approvalCount: number;
currentOperator: Address | undefined;
isLoading: boolean;
};

const getStatusColor = (status: OperatorApprovalStatus) => {
switch (status) {
case 'approved':
return 'green';
case 'rejected':
return 'red';
case 'pending':
default:
return 'yellow';
}
};

const getStatusLabel = (status: OperatorApprovalStatus) => {
switch (status) {
case 'approved':
return 'Approved';
case 'rejected':
return 'Rejected';
case 'pending':
default:
return 'Pending';
}
};

const OperatorStatusSection: FC<Props> = ({
operatorCandidates,
approvedOperators,
rejectedOperators,
approvalCount,
currentOperator,
isLoading,
}) => {
const operatorsWithStatus = useMemo((): OperatorWithStatus[] => {
const approvedSet = new Set(
approvedOperators.map((addr) => addr.toLowerCase()),
);
const rejectedSet = new Set(
rejectedOperators.map((addr) => addr.toLowerCase()),
);

return operatorCandidates.map((address) => {
const addrLower = address.toLowerCase();
let status: OperatorApprovalStatus = 'pending';

if (approvedSet.has(addrLower)) {
status = 'approved';
} else if (rejectedSet.has(addrLower)) {
status = 'rejected';
}

return { address, status };
});
}, [operatorCandidates, approvedOperators, rejectedOperators]);

if (isLoading) {
return (
<div className="space-y-2">
<Typography variant="h5" className="text-mono-200 dark:text-mono-0">
Operator Status
</Typography>

<SkeletonLoader className="h-5 w-32 mb-2" />

<div className="space-y-2">
<SkeletonLoader className="h-10 w-full" />
<SkeletonLoader className="h-10 w-full" />
</div>
</div>
);
}

const totalOperators = operatorCandidates.length;

return (
<div className="space-y-2">
<Typography variant="h5" className="text-mono-200 dark:text-mono-0">
Operator Status
</Typography>

<div className="flex items-center gap-2 mb-2">
<span className="text-sm text-mono-140 dark:text-mono-80">
Progress:
</span>

<span className="text-sm font-semibold">
{approvalCount}/{totalOperators} operators approved
</span>
</div>

<div className="space-y-2 max-h-40 overflow-y-auto">
{operatorsWithStatus.map(({ address, status }) => {
const isCurrentOperator =
currentOperator?.toLowerCase() === address.toLowerCase();

return (
<div
key={address}
className={`flex items-center justify-between p-2 rounded-lg ${
isCurrentOperator
? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
: 'bg-mono-20 dark:bg-mono-180'
}`}
>
<div className="flex items-center gap-2">
<Avatar
sourceVariant="address"
value={address}
size="sm"
theme="substrate"
/>

<span className="text-sm font-mono">
{shortenString(address, 6)}
</span>

{isCurrentOperator && (
<span className="text-xs text-blue-600 dark:text-blue-400">
(You)
</span>
)}
</div>

<Chip color={getStatusColor(status)}>
{getStatusLabel(status)}
</Chip>
</div>
);
})}
</div>
</div>
);
};

export default OperatorStatusSection;
Loading