Skip to content
56 changes: 56 additions & 0 deletions src/app/(rucio)/subscription/list/ListSubscriptionClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import { ListSubscription } from '@/component-library/pages/Subscriptions/list/ListSubscription';
import { useRouter, usePathname } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
import { SiteHeaderViewModel } from '@/lib/infrastructure/data/view-model/site-header';

export interface ListSubscriptionClientProps {
accountFilter?: string;
autoSearch?: boolean;
}

export const ListSubscriptionClient = (props: ListSubscriptionClientProps) => {
const router = useRouter();
const pathname = usePathname();
const hasUpdatedUrl = useRef(false);

// Fetch site header to get active account if accountFilter is not provided
const querySiteHeader = async () => {
const res = await fetch('/api/feature/get-site-header');
return res.json();
};

const {
data: siteHeader,
isFetching: isSiteHeaderFetching,
} = useQuery<SiteHeaderViewModel>({
queryKey: ['subscription-account-client'],
queryFn: querySiteHeader,
retry: false,
refetchOnWindowFocus: false,
enabled: !props.accountFilter, // Only fetch if accountFilter is not provided
});

// Determine the account to use
const account = props.accountFilter || siteHeader?.activeAccount?.rucioAccount;

// Update URL to include account and autoSearch parameters
useEffect(() => {
if (!hasUpdatedUrl.current && account && !isSiteHeaderFetching) {
hasUpdatedUrl.current = true;

// Build URL parameters
const urlParams = new URLSearchParams();
urlParams.set('account', account);
urlParams.set('autoSearch', 'true');

// Update the URL using router.replace (doesn't add to history)
const newUrl = `${pathname}?${urlParams.toString()}`;
router.replace(newUrl);
}
}, [account, isSiteHeaderFetching, pathname, router]);

return <ListSubscription accountFilter={account} autoSearch={props.autoSearch !== false} />;
};
27 changes: 23 additions & 4 deletions src/app/(rucio)/subscription/list/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
'use client';
import { ListSubscription } from '@/component-library/pages/Subscriptions/list/ListSubscription';
import { ListSubscriptionClient } from './ListSubscriptionClient';

export default function Page() {
return <ListSubscription />;
export default async function Page({ searchParams }: { searchParams?: Promise<{ [key: string]: string | string[] | undefined }> }) {
const params = await searchParams;
const accountParam = typeof params?.['account'] === 'string' ? params['account'] : undefined;
const autoSearch = params?.['autoSearch'] === 'true';

return (
<main className="min-h-screen bg-neutral-0 dark:bg-neutral-900 transition-colors duration-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8 lg:py-10">
<header className="mb-8">
<h1 className="text-3xl sm:text-4xl font-bold text-neutral-900 dark:text-neutral-100 mb-2">Subscriptions</h1>
<p className="text-base sm:text-lg text-neutral-600 dark:text-neutral-400">View and manage subscriptions.</p>
</header>
<section aria-label="Subscription List">
<ListSubscriptionClient accountFilter={accountParam} autoSearch={autoSearch} />
</section>
</div>
</main>
);
}

export const metadata = {
title: 'Subscriptions List - Rucio',
};
73 changes: 3 additions & 70 deletions src/app/(rucio)/subscription/page/[account]/[name]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,10 @@
'use client';

import { use, useEffect, useState } from 'react';
import { PageSubscription as PageSubscriptionStory } from '@/component-library/pages/legacy/Subscriptions/PageSubscription';
import { SubscriptionViewModel } from '@/lib/infrastructure/data/view-model/subscriptions';
import { Loading } from '@/component-library/pages/legacy/Helpers/Loading';

async function updateSubscription(id: string, filter: string, replicationRules: string) {
const req: any = {
method: 'PUT',
url: new URL(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/mock-update-subscription`),
headers: {
'Content-Type': 'application/json',
},
params: {
subscriptionID: id,
},
body: {
filter: filter,
replicationRules: replicationRules,
},
};
const res = await fetch(req.url, {
method: 'PUT',
headers: new Headers({
'Content-Type': 'application/json',
} as HeadersInit),
body: JSON.stringify(req.body) as BodyInit,
});

return await res.json();
}
import { use } from 'react';
import { DetailsSubscription } from '@/component-library/pages/Subscriptions/details/DetailsSubscription';

export default function PageSubscription({ params }: { params: Promise<{ account: string; name: string }> }) {
const { account, name } = use(params);
const [subscriptionViewModel, setSubscriptionViewModel] = useState<SubscriptionViewModel>({ status: 'pending' } as SubscriptionViewModel);
useEffect(() => {
subscriptionQuery(account, name).then(setSubscriptionViewModel);
}, [account, name]);
async function subscriptionQuery(account: string, name: string): Promise<SubscriptionViewModel> {
const req: any = {
method: 'GET',
url: new URL(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/get-subscription`),
params: {
account: account,
name: name,
},
headers: new Headers({
'Content-Type': 'application/json',
} as HeadersInit),
};

const res = await fetch(req.url, {
method: 'GET',
headers: new Headers({
'Content-Type': 'application/json',
} as HeadersInit),
});

return await res.json();
}
if (subscriptionViewModel.status === 'success') {
return (
<PageSubscriptionStory
subscriptionViewModel={subscriptionViewModel}
editFilter={(s: string) => {
updateSubscription(subscriptionViewModel.id, s, subscriptionViewModel.replication_rules).then(setSubscriptionViewModel);
}}
editReplicationRules={(r: string) => {
updateSubscription(subscriptionViewModel.id, subscriptionViewModel.filter, r).then(setSubscriptionViewModel);
}}
/>
);
} else {
return <Loading title="View Subscription" subtitle={`For subscription ${name}`} />;
}
return <DetailsSubscription account={account} name={name} />;
}
10 changes: 7 additions & 3 deletions src/app/api/feature/list-subscription-rule-states/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { getSessionUser } from '@/lib/infrastructure/auth/nextauth-session-utils

/**
* GET /api/feature/list-subscription-rule-states
* Query params: account, name
* Returns rule states for subscriptions of the authenticated user's account
* Query params: account (optional - defaults to authenticated user's account), name
* Returns rule states for subscriptions of the specified account
*/
export async function GET(request: NextRequest) {
try {
Expand All @@ -20,7 +20,11 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const account = sessionUser.rucioAccount;
// Check for account query parameter first, fall back to session user's account
const { searchParams } = new URL(request.url);
const accountParam = searchParams.get('account');
const account = accountParam || sessionUser.rucioAccount;

if (!account) {
return NextResponse.json({ error: 'Could not determine account name. Are you logged in?' }, { status: 400 });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,15 @@ export const InForm: Story = {
return (
<div className="w-96 p-6 rounded-lg bg-neutral-100 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 space-y-4">
<div>
<label htmlFor="start-time" className="text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2 block">Start Time</label>
<label htmlFor="start-time" className="text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2 block">
Start Time
</label>
<TimeInput id="start-time" onchange={setStartTime} initialtime={startTime} showSeconds={false} />
</div>
<div>
<label htmlFor="end-time" className="text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2 block">End Time</label>
<label htmlFor="end-time" className="text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2 block">
End Time
</label>
<TimeInput id="end-time" onchange={setEndTime} initialtime={endTime} showSeconds={false} />
</div>
<div className="text-sm text-neutral-700 dark:text-neutral-300 pt-2 border-t border-neutral-200 dark:border-neutral-700">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { SubscriptionState } from '@/lib/core/entity/rucio';
import React from 'react';
import { Badge } from '@/component-library/atoms/misc/Badge';

const stateString: Record<SubscriptionState, string> = {
[SubscriptionState.ACTIVE]: 'Active',
[SubscriptionState.INACTIVE]: 'Inactive',
[SubscriptionState.NEW]: 'New',
[SubscriptionState.UPDATED]: 'Updated',
[SubscriptionState.BROKEN]: 'Broken',
[SubscriptionState.UNKNOWN]: 'Unknown',
};

/**
* Maps subscription states to semantic badge variants from the design system.
*
* Semantic color assignments:
* - UPDATED: Success (green) - Most common state, subscription has created rules
* - ACTIVE: Info (brand purple) - Subscription is active and running
* - NEW: Info (brand purple) - New subscription (tied to retroactive option)
* - INACTIVE: Neutral (gray) - Subscription is disabled
* - BROKEN: Error (red) - Subscription is in error state
* - UNKNOWN: Neutral (gray) - Undefined state
*/
const stateVariants: Record<SubscriptionState, 'default' | 'success' | 'error' | 'warning' | 'info' | 'neutral'> = {
[SubscriptionState.UPDATED]: 'success',
[SubscriptionState.ACTIVE]: 'info',
[SubscriptionState.NEW]: 'info',
[SubscriptionState.INACTIVE]: 'neutral',
[SubscriptionState.BROKEN]: 'error',
[SubscriptionState.UNKNOWN]: 'neutral',
};

export const SubscriptionStateBadge = (props: { value: SubscriptionState; className?: string }) => {
return <Badge value={stateString[props.value]} variant={stateVariants[props.value]} className={props.className} />;
};
22 changes: 20 additions & 2 deletions src/component-library/features/command-palette/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { getCards } from '@/lib/utils/hotbar-storage';
import { getNavigationCommands, getActionCommands, getHelpCommands } from '@/lib/infrastructure/command-palette/command-registry';
import { navigateToSearch } from '@/lib/infrastructure/utils/navigation';
import { ClockIcon, BookmarkIcon } from '@heroicons/react/24/outline';
import { useQuery } from '@tanstack/react-query';
import { SiteHeaderViewModel } from '@/lib/infrastructure/data/view-model/site-header';

export interface CommandPaletteProps {
/** Whether the palette is open */
Expand All @@ -32,6 +34,22 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ open, onOpenChan
const [searchQuery, setSearchQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);

// Fetch site header to get active account
const querySiteHeader = async () => {
const res = await fetch('/api/feature/get-site-header');
return res.json();
};

const { data: siteHeader } = useQuery<SiteHeaderViewModel>({
queryKey: ['command-palette-site-header'],
queryFn: querySiteHeader,
retry: false,
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000, // 5 minutes
});

const account = siteHeader?.activeAccount?.rucioAccount;

// Build all sections with filtering
const sections = useMemo<CommandSection[]>(() => {
const allSections: CommandSection[] = [];
Expand Down Expand Up @@ -80,7 +98,7 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ open, onOpenChan
}

// Navigation Section
const navigationItems = getNavigationCommands();
const navigationItems = getNavigationCommands(account);
allSections.push({
id: 'navigation',
title: 'Navigation',
Expand Down Expand Up @@ -122,7 +140,7 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ open, onOpenChan
}

return allSections;
}, [searchQuery]);
}, [searchQuery, account]);

// Calculate total items count for keyboard navigation
const totalItems = useMemo(() => {
Expand Down
Loading
Loading