@djangocfg/layouts 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +47 -65
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +118 -163
- package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +31 -45
- package/src/layouts/PaymentsLayout/components/index.ts +1 -4
- package/src/layouts/PaymentsLayout/events.ts +23 -84
- package/src/layouts/PaymentsLayout/index.ts +7 -11
- package/src/layouts/PaymentsLayout/types.ts +3 -16
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +45 -16
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +16 -12
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +0 -2
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +3 -6
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +50 -30
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +0 -1
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +1 -2
- package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +273 -0
- package/src/layouts/PaymentsLayout/views/transactions/components/index.ts +1 -0
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +5 -17
- package/src/layouts/PaymentsLayout/README.md +0 -133
- package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +0 -172
- package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +0 -100
- package/src/layouts/PaymentsLayout/context/RootPaymentsContext.tsx +0 -129
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +0 -109
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +0 -194
- package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +0 -3
- package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +0 -19
- package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +0 -103
- package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +0 -29
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create API Key Dialog
|
|
3
|
-
* Dialog for creating new API keys
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import React, { useState, useEffect } from 'react';
|
|
9
|
-
import {
|
|
10
|
-
Dialog,
|
|
11
|
-
DialogContent,
|
|
12
|
-
DialogDescription,
|
|
13
|
-
DialogFooter,
|
|
14
|
-
DialogHeader,
|
|
15
|
-
DialogTitle,
|
|
16
|
-
Form,
|
|
17
|
-
FormControl,
|
|
18
|
-
FormDescription,
|
|
19
|
-
FormField,
|
|
20
|
-
FormItem,
|
|
21
|
-
FormLabel,
|
|
22
|
-
FormMessage,
|
|
23
|
-
Input,
|
|
24
|
-
Button,
|
|
25
|
-
useEventListener,
|
|
26
|
-
} from '@djangocfg/ui';
|
|
27
|
-
import { Plus, RefreshCw } from 'lucide-react';
|
|
28
|
-
import { useForm } from 'react-hook-form';
|
|
29
|
-
import { zodResolver } from '@hookform/resolvers/zod';
|
|
30
|
-
import { useApiKeysContext } from '@djangocfg/api/cfg/contexts';
|
|
31
|
-
import { Schemas } from '@djangocfg/api/cfg/generated';
|
|
32
|
-
import { paymentsLogger } from '../../../utils/logger';
|
|
33
|
-
|
|
34
|
-
const { APIKeyCreateRequestSchema } = Schemas;
|
|
35
|
-
type APIKeyCreateRequest = Schemas.APIKeyCreateRequest;
|
|
36
|
-
import { PAYMENTS_DIALOG_EVENTS, closePaymentsDialog } from '../events';
|
|
37
|
-
|
|
38
|
-
export const CreateApiKeyDialog: React.FC = () => {
|
|
39
|
-
const [open, setOpen] = useState(false);
|
|
40
|
-
const [initialData, setInitialData] = useState<any>();
|
|
41
|
-
const { createApiKey } = useApiKeysContext();
|
|
42
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
43
|
-
|
|
44
|
-
const form = useForm<APIKeyCreateRequest>({
|
|
45
|
-
resolver: zodResolver(APIKeyCreateRequestSchema),
|
|
46
|
-
defaultValues: {
|
|
47
|
-
name: '',
|
|
48
|
-
expires_in_days: null,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Event listeners
|
|
53
|
-
useEventListener(PAYMENTS_DIALOG_EVENTS.OPEN_CREATE_APIKEY_DIALOG, (event: any) => {
|
|
54
|
-
setInitialData(event.payload?.initialKeyData);
|
|
55
|
-
setOpen(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
useEventListener(PAYMENTS_DIALOG_EVENTS.CLOSE_PAYMENTS_DIALOG, () => {
|
|
59
|
-
setOpen(false);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Reset form when initial data changes
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (initialData) {
|
|
65
|
-
form.reset({
|
|
66
|
-
name: initialData.name || '',
|
|
67
|
-
expires_in_days: initialData.expires_in_days || undefined,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}, [initialData, form]);
|
|
71
|
-
|
|
72
|
-
const handleClose = () => {
|
|
73
|
-
setOpen(false);
|
|
74
|
-
form.reset();
|
|
75
|
-
setInitialData(undefined);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const handleSubmit = async (data: APIKeyCreateRequest) => {
|
|
79
|
-
try {
|
|
80
|
-
setIsSubmitting(true);
|
|
81
|
-
await createApiKey(data);
|
|
82
|
-
handleClose();
|
|
83
|
-
closePaymentsDialog();
|
|
84
|
-
} catch (error) {
|
|
85
|
-
paymentsLogger.error('Failed to create API key:', error);
|
|
86
|
-
} finally {
|
|
87
|
-
setIsSubmitting(false);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}>
|
|
93
|
-
<DialogContent className="sm:max-w-md">
|
|
94
|
-
<DialogHeader>
|
|
95
|
-
<DialogTitle>Create API Key</DialogTitle>
|
|
96
|
-
<DialogDescription>
|
|
97
|
-
Create a new API key for secure access to the platform.
|
|
98
|
-
</DialogDescription>
|
|
99
|
-
</DialogHeader>
|
|
100
|
-
|
|
101
|
-
<Form {...form}>
|
|
102
|
-
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
|
103
|
-
<FormField
|
|
104
|
-
control={form.control}
|
|
105
|
-
name="name"
|
|
106
|
-
render={({ field }) => (
|
|
107
|
-
<FormItem>
|
|
108
|
-
<FormLabel>Name</FormLabel>
|
|
109
|
-
<FormControl>
|
|
110
|
-
<Input placeholder="My API Key" {...field} />
|
|
111
|
-
</FormControl>
|
|
112
|
-
<FormDescription>
|
|
113
|
-
A descriptive name for your API key.
|
|
114
|
-
</FormDescription>
|
|
115
|
-
<FormMessage />
|
|
116
|
-
</FormItem>
|
|
117
|
-
)}
|
|
118
|
-
/>
|
|
119
|
-
|
|
120
|
-
<FormField
|
|
121
|
-
control={form.control}
|
|
122
|
-
name="expires_in_days"
|
|
123
|
-
render={({ field }) => (
|
|
124
|
-
<FormItem>
|
|
125
|
-
<FormLabel>Expiration (Days)</FormLabel>
|
|
126
|
-
<FormControl>
|
|
127
|
-
<Input
|
|
128
|
-
type="number"
|
|
129
|
-
placeholder="30"
|
|
130
|
-
min="1"
|
|
131
|
-
max="365"
|
|
132
|
-
{...field}
|
|
133
|
-
onChange={(e) => {
|
|
134
|
-
const value = e.target.value;
|
|
135
|
-
field.onChange(value ? parseInt(value, 10) : null);
|
|
136
|
-
}}
|
|
137
|
-
value={field.value || ''}
|
|
138
|
-
/>
|
|
139
|
-
</FormControl>
|
|
140
|
-
<FormDescription>
|
|
141
|
-
Number of days until expiration. Leave empty for no expiration.
|
|
142
|
-
</FormDescription>
|
|
143
|
-
<FormMessage />
|
|
144
|
-
</FormItem>
|
|
145
|
-
)}
|
|
146
|
-
/>
|
|
147
|
-
|
|
148
|
-
<DialogFooter>
|
|
149
|
-
<Button type="button" variant="outline" onClick={handleClose}>
|
|
150
|
-
Cancel
|
|
151
|
-
</Button>
|
|
152
|
-
<Button type="submit" disabled={isSubmitting || !form.formState.isValid}>
|
|
153
|
-
{isSubmitting ? (
|
|
154
|
-
<>
|
|
155
|
-
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
|
156
|
-
Creating...
|
|
157
|
-
</>
|
|
158
|
-
) : (
|
|
159
|
-
<>
|
|
160
|
-
<Plus className="h-4 w-4 mr-2" />
|
|
161
|
-
Create Key
|
|
162
|
-
</>
|
|
163
|
-
)}
|
|
164
|
-
</Button>
|
|
165
|
-
</DialogFooter>
|
|
166
|
-
</form>
|
|
167
|
-
</Form>
|
|
168
|
-
</DialogContent>
|
|
169
|
-
</Dialog>
|
|
170
|
-
);
|
|
171
|
-
};
|
|
172
|
-
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Delete API Key Dialog
|
|
3
|
-
* Confirmation dialog for deleting API keys
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import React, { useState } from 'react';
|
|
9
|
-
import {
|
|
10
|
-
Dialog,
|
|
11
|
-
DialogContent,
|
|
12
|
-
DialogDescription,
|
|
13
|
-
DialogFooter,
|
|
14
|
-
DialogHeader,
|
|
15
|
-
DialogTitle,
|
|
16
|
-
Button,
|
|
17
|
-
useEventListener,
|
|
18
|
-
} from '@djangocfg/ui';
|
|
19
|
-
import { Trash2, RefreshCw } from 'lucide-react';
|
|
20
|
-
import { useApiKeysContext } from '@djangocfg/api/cfg/contexts';
|
|
21
|
-
import { paymentsLogger } from '../../../utils/logger';
|
|
22
|
-
import { PAYMENTS_DIALOG_EVENTS, closePaymentsDialog } from '../events';
|
|
23
|
-
|
|
24
|
-
export const DeleteApiKeyDialog: React.FC = () => {
|
|
25
|
-
const [open, setOpen] = useState(false);
|
|
26
|
-
const [keyId, setKeyId] = useState<string>('');
|
|
27
|
-
const [isDeleting, setIsDeleting] = useState(false);
|
|
28
|
-
const { deleteApiKey } = useApiKeysContext();
|
|
29
|
-
|
|
30
|
-
useEventListener(PAYMENTS_DIALOG_EVENTS.OPEN_DELETE_APIKEY_DIALOG, (payload: any) => {
|
|
31
|
-
const id = payload?.keyId || '';
|
|
32
|
-
setKeyId(id);
|
|
33
|
-
setOpen(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
useEventListener(PAYMENTS_DIALOG_EVENTS.CLOSE_PAYMENTS_DIALOG, () => {
|
|
37
|
-
setOpen(false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const handleClose = () => {
|
|
41
|
-
setOpen(false);
|
|
42
|
-
setKeyId('');
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const handleDelete = async () => {
|
|
46
|
-
if (!keyId) return;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
setIsDeleting(true);
|
|
50
|
-
await deleteApiKey(keyId);
|
|
51
|
-
// Add a small delay to ensure the list refreshes before closing
|
|
52
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
53
|
-
handleClose();
|
|
54
|
-
closePaymentsDialog();
|
|
55
|
-
} catch (error) {
|
|
56
|
-
paymentsLogger.error('Failed to delete API key:', error);
|
|
57
|
-
} finally {
|
|
58
|
-
setIsDeleting(false);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}>
|
|
64
|
-
<DialogContent className="sm:max-w-md">
|
|
65
|
-
<DialogHeader>
|
|
66
|
-
<DialogTitle>Delete API Key</DialogTitle>
|
|
67
|
-
<DialogDescription>
|
|
68
|
-
Are you sure you want to delete this API key? This action cannot be undone and will
|
|
69
|
-
immediately revoke access for any applications using this key.
|
|
70
|
-
</DialogDescription>
|
|
71
|
-
</DialogHeader>
|
|
72
|
-
|
|
73
|
-
<DialogFooter>
|
|
74
|
-
<Button type="button" variant="outline" onClick={handleClose} disabled={isDeleting}>
|
|
75
|
-
Cancel
|
|
76
|
-
</Button>
|
|
77
|
-
<Button
|
|
78
|
-
type="button"
|
|
79
|
-
variant="destructive"
|
|
80
|
-
onClick={handleDelete}
|
|
81
|
-
disabled={isDeleting}
|
|
82
|
-
>
|
|
83
|
-
{isDeleting ? (
|
|
84
|
-
<>
|
|
85
|
-
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
|
86
|
-
Deleting...
|
|
87
|
-
</>
|
|
88
|
-
) : (
|
|
89
|
-
<>
|
|
90
|
-
<Trash2 className="h-4 w-4 mr-2" />
|
|
91
|
-
Delete Key
|
|
92
|
-
</>
|
|
93
|
-
)}
|
|
94
|
-
</Button>
|
|
95
|
-
</DialogFooter>
|
|
96
|
-
</DialogContent>
|
|
97
|
-
</Dialog>
|
|
98
|
-
);
|
|
99
|
-
};
|
|
100
|
-
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { createContext, useContext, useEffect, type ReactNode } from 'react';
|
|
4
|
-
import { api, Hooks } from '@djangocfg/api';
|
|
5
|
-
import type { API } from '@djangocfg/api';
|
|
6
|
-
import type {
|
|
7
|
-
Currency,
|
|
8
|
-
PaginatedCurrencyListList,
|
|
9
|
-
ProviderCurrency,
|
|
10
|
-
PaginatedProviderCurrencyList,
|
|
11
|
-
Network,
|
|
12
|
-
PaginatedNetworkList,
|
|
13
|
-
} from '@djangocfg/api';
|
|
14
|
-
|
|
15
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
16
|
-
// Context Type
|
|
17
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
export interface RootPaymentsContextValue {
|
|
20
|
-
// Currencies
|
|
21
|
-
currencies: PaginatedCurrencyListList | undefined;
|
|
22
|
-
isLoadingCurrencies: boolean;
|
|
23
|
-
currenciesError: Error | undefined;
|
|
24
|
-
refreshCurrencies: () => Promise<void>;
|
|
25
|
-
|
|
26
|
-
// Provider Currencies
|
|
27
|
-
providerCurrencies: PaginatedProviderCurrencyList | undefined;
|
|
28
|
-
isLoadingProviderCurrencies: boolean;
|
|
29
|
-
providerCurrenciesError: Error | undefined;
|
|
30
|
-
refreshProviderCurrencies: () => Promise<void>;
|
|
31
|
-
|
|
32
|
-
// Networks
|
|
33
|
-
networks: PaginatedNetworkList | undefined;
|
|
34
|
-
isLoadingNetworks: boolean;
|
|
35
|
-
networksError: Error | undefined;
|
|
36
|
-
refreshNetworks: () => Promise<void>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
40
|
-
// Context
|
|
41
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
const RootPaymentsContext = createContext<RootPaymentsContextValue | undefined>(undefined);
|
|
44
|
-
|
|
45
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
46
|
-
// Provider
|
|
47
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
export function RootPaymentsProvider({ children }: { children: ReactNode }) {
|
|
50
|
-
// List all currencies
|
|
51
|
-
const {
|
|
52
|
-
data: currencies,
|
|
53
|
-
error: currenciesError,
|
|
54
|
-
isLoading: isLoadingCurrencies,
|
|
55
|
-
mutate: mutateCurrencies,
|
|
56
|
-
} = Hooks.usePaymentsCurrenciesList({}, api as unknown as API);
|
|
57
|
-
|
|
58
|
-
// List all provider currencies
|
|
59
|
-
const {
|
|
60
|
-
data: providerCurrencies,
|
|
61
|
-
error: providerCurrenciesError,
|
|
62
|
-
isLoading: isLoadingProviderCurrencies,
|
|
63
|
-
mutate: mutateProviderCurrencies,
|
|
64
|
-
} = Hooks.usePaymentsProviderCurrenciesList({}, api as unknown as API);
|
|
65
|
-
|
|
66
|
-
// List all networks
|
|
67
|
-
const {
|
|
68
|
-
data: networks,
|
|
69
|
-
error: networksError,
|
|
70
|
-
isLoading: isLoadingNetworks,
|
|
71
|
-
mutate: mutateNetworks,
|
|
72
|
-
} = Hooks.usePaymentsNetworksList({}, api as unknown as API);
|
|
73
|
-
|
|
74
|
-
const refreshCurrencies = async () => {
|
|
75
|
-
await mutateCurrencies();
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const refreshProviderCurrencies = async () => {
|
|
79
|
-
await mutateProviderCurrencies();
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const refreshNetworks = async () => {
|
|
83
|
-
await mutateNetworks();
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const value: RootPaymentsContextValue = {
|
|
87
|
-
currencies,
|
|
88
|
-
isLoadingCurrencies,
|
|
89
|
-
currenciesError,
|
|
90
|
-
refreshCurrencies,
|
|
91
|
-
providerCurrencies,
|
|
92
|
-
isLoadingProviderCurrencies,
|
|
93
|
-
providerCurrenciesError,
|
|
94
|
-
refreshProviderCurrencies,
|
|
95
|
-
networks,
|
|
96
|
-
isLoadingNetworks,
|
|
97
|
-
networksError,
|
|
98
|
-
refreshNetworks,
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<RootPaymentsContext.Provider value={value}>{children}</RootPaymentsContext.Provider>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
107
|
-
// Hook
|
|
108
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
export function useRootPaymentsContext(): RootPaymentsContextValue {
|
|
111
|
-
const context = useContext(RootPaymentsContext);
|
|
112
|
-
if (!context) {
|
|
113
|
-
throw new Error('useRootPaymentsContext must be used within RootPaymentsProvider');
|
|
114
|
-
}
|
|
115
|
-
return context;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
119
|
-
// Re-export types
|
|
120
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
export type {
|
|
123
|
-
Currency,
|
|
124
|
-
PaginatedCurrencyListList,
|
|
125
|
-
ProviderCurrency,
|
|
126
|
-
PaginatedProviderCurrencyList,
|
|
127
|
-
Network,
|
|
128
|
-
PaginatedNetworkList,
|
|
129
|
-
};
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API Key Metrics Component
|
|
3
|
-
* Display API key usage statistics
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import React from 'react';
|
|
9
|
-
import {
|
|
10
|
-
Card,
|
|
11
|
-
CardContent,
|
|
12
|
-
CardHeader,
|
|
13
|
-
CardTitle,
|
|
14
|
-
Skeleton,
|
|
15
|
-
} from '@djangocfg/ui';
|
|
16
|
-
import { Key, CheckCircle2, XCircle, Activity } from 'lucide-react';
|
|
17
|
-
import { useApiKeysContext } from '@djangocfg/api/cfg/contexts';
|
|
18
|
-
|
|
19
|
-
export const ApiKeyMetrics: React.FC = () => {
|
|
20
|
-
const { apiKeys, isLoadingApiKeys } = useApiKeysContext();
|
|
21
|
-
|
|
22
|
-
const keysList = apiKeys?.results || [];
|
|
23
|
-
|
|
24
|
-
const metrics = React.useMemo(() => {
|
|
25
|
-
const total = keysList.length;
|
|
26
|
-
const active = keysList.filter(key => key.is_active).length;
|
|
27
|
-
const inactive = total - active;
|
|
28
|
-
const recentlyUsed = keysList.filter(key => {
|
|
29
|
-
if (!key.last_used_at) return false;
|
|
30
|
-
const lastUsed = new Date(key.last_used_at);
|
|
31
|
-
const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
32
|
-
return lastUsed > dayAgo;
|
|
33
|
-
}).length;
|
|
34
|
-
|
|
35
|
-
return { total, active, inactive, recentlyUsed };
|
|
36
|
-
}, [keysList]);
|
|
37
|
-
|
|
38
|
-
if (isLoadingApiKeys) {
|
|
39
|
-
return (
|
|
40
|
-
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
41
|
-
{Array.from({ length: 4 }).map((_, i) => (
|
|
42
|
-
<Card key={i}>
|
|
43
|
-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
44
|
-
<Skeleton className="h-4 w-24" />
|
|
45
|
-
<Skeleton className="h-4 w-4 rounded-full" />
|
|
46
|
-
</CardHeader>
|
|
47
|
-
<CardContent>
|
|
48
|
-
<Skeleton className="h-7 w-12 mb-1" />
|
|
49
|
-
<Skeleton className="h-3 w-32" />
|
|
50
|
-
</CardContent>
|
|
51
|
-
</Card>
|
|
52
|
-
))}
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const metricsData = [
|
|
58
|
-
{
|
|
59
|
-
title: 'Total Keys',
|
|
60
|
-
value: metrics.total.toString(),
|
|
61
|
-
description: 'All API keys',
|
|
62
|
-
icon: Key,
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
title: 'Active Keys',
|
|
66
|
-
value: metrics.active.toString(),
|
|
67
|
-
description: 'Currently active',
|
|
68
|
-
icon: CheckCircle2,
|
|
69
|
-
iconColor: 'text-green-600',
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
title: 'Inactive Keys',
|
|
73
|
-
value: metrics.inactive.toString(),
|
|
74
|
-
description: 'Not in use',
|
|
75
|
-
icon: XCircle,
|
|
76
|
-
iconColor: 'text-muted-foreground',
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
title: 'Recently Used',
|
|
80
|
-
value: metrics.recentlyUsed.toString(),
|
|
81
|
-
description: 'Last 24 hours',
|
|
82
|
-
icon: Activity,
|
|
83
|
-
iconColor: 'text-blue-600',
|
|
84
|
-
},
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
89
|
-
{metricsData.map((metric, index) => {
|
|
90
|
-
const Icon = metric.icon;
|
|
91
|
-
return (
|
|
92
|
-
<Card key={index}>
|
|
93
|
-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
94
|
-
<CardTitle className="text-sm font-medium">{metric.title}</CardTitle>
|
|
95
|
-
<Icon className={`h-4 w-4 ${metric.iconColor || 'text-muted-foreground'}`} />
|
|
96
|
-
</CardHeader>
|
|
97
|
-
<CardContent>
|
|
98
|
-
<div className="text-2xl font-bold">{metric.value}</div>
|
|
99
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
100
|
-
{metric.description}
|
|
101
|
-
</p>
|
|
102
|
-
</CardContent>
|
|
103
|
-
</Card>
|
|
104
|
-
);
|
|
105
|
-
})}
|
|
106
|
-
</div>
|
|
107
|
-
);
|
|
108
|
-
};
|
|
109
|
-
|