@datalayer/core 1.0.24 → 1.0.25

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.
Files changed (61) hide show
  1. package/README.md +25 -2
  2. package/lib/api/constants.d.ts +3 -0
  3. package/lib/api/constants.js +3 -0
  4. package/lib/api/index.d.ts +1 -0
  5. package/lib/api/index.js +1 -0
  6. package/lib/api/runtimes/checkpoints.d.ts +4 -4
  7. package/lib/api/runtimes/runtimes.d.ts +3 -3
  8. package/lib/api/scheduler/index.d.ts +6 -0
  9. package/lib/api/scheduler/index.js +10 -0
  10. package/lib/api/scheduler/schedules.d.ts +85 -0
  11. package/lib/api/scheduler/schedules.js +78 -0
  12. package/lib/components/checkout/StripeCheckout.js +48 -2
  13. package/lib/components/display/LiveRelativeTime.d.ts +1 -10
  14. package/lib/components/display/LiveRelativeTime.js +3 -18
  15. package/lib/components/index.d.ts +3 -0
  16. package/lib/components/index.js +3 -0
  17. package/lib/components/scheduler/ScheduleMenu.d.ts +54 -0
  18. package/lib/components/scheduler/ScheduleMenu.js +154 -0
  19. package/lib/components/scheduler/index.d.ts +1 -0
  20. package/lib/{views/iam-tokens → components/scheduler}/index.js +1 -3
  21. package/lib/components/time/LiveRelativeTime.d.ts +14 -0
  22. package/lib/components/time/LiveRelativeTime.js +39 -0
  23. package/lib/components/time/index.d.ts +1 -0
  24. package/lib/components/time/index.js +5 -0
  25. package/lib/components/timeline/Timeline.d.ts +20 -0
  26. package/lib/components/timeline/Timeline.js +90 -0
  27. package/lib/components/timeline/index.d.ts +1 -0
  28. package/lib/components/timeline/index.js +5 -0
  29. package/lib/hooks/useCache.d.ts +1 -1
  30. package/lib/hooks/useCache.js +17 -13
  31. package/lib/hooks/useProjectStore.d.ts +1 -1
  32. package/lib/hooks/useProjects.d.ts +2 -2
  33. package/lib/models/IAMToken.d.ts +1 -1
  34. package/lib/models/ProjectDTO.d.ts +1 -1
  35. package/lib/models/ProjectDTO.js +1 -1
  36. package/lib/state/substates/CoreState.js +1 -1
  37. package/lib/utils/Date.d.ts +7 -0
  38. package/lib/utils/Date.js +43 -10
  39. package/lib/utils/index.d.ts +1 -0
  40. package/lib/utils/index.js +1 -0
  41. package/lib/views/api-keys/APIKeyEdit.d.ts +8 -0
  42. package/lib/views/{iam-tokens/IAMTokenEdit.js → api-keys/APIKeyEdit.js} +31 -31
  43. package/lib/views/api-keys/APIKeyNew.d.ts +8 -0
  44. package/lib/views/api-keys/APIKeyNew.js +143 -0
  45. package/lib/views/api-keys/APIKeys.d.ts +12 -0
  46. package/lib/views/api-keys/APIKeys.js +119 -0
  47. package/lib/views/api-keys/APIKeysStandalone.d.ts +2 -0
  48. package/lib/views/api-keys/APIKeysStandalone.js +117 -0
  49. package/lib/views/api-keys/index.d.ts +3 -0
  50. package/lib/views/api-keys/index.js +7 -0
  51. package/package.json +1 -1
  52. package/lib/views/iam-tokens/IAMTokenEdit.d.ts +0 -6
  53. package/lib/views/iam-tokens/IAMTokenNew.d.ts +0 -8
  54. package/lib/views/iam-tokens/IAMTokenNew.js +0 -118
  55. package/lib/views/iam-tokens/IAMTokens.d.ts +0 -12
  56. package/lib/views/iam-tokens/IAMTokens.js +0 -87
  57. package/lib/views/iam-tokens/Tokens.d.ts +0 -2
  58. package/lib/views/iam-tokens/Tokens.js +0 -85
  59. package/lib/views/iam-tokens/index.d.ts +0 -3
  60. /package/lib/utils/{logger.d.ts → Logger.d.ts} +0 -0
  61. /package/lib/utils/{logger.js → Logger.js} +0 -0
@@ -0,0 +1,154 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2023-2025 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ import { useEffect, useMemo, useState } from 'react';
7
+ import { ActionList, ActionMenu, Box, Button, Dialog, FormControl, IconButton, Spinner, Text, TextInput, } from '@primer/react';
8
+ import { ClockIcon, TrashIcon } from '@primer/octicons-react';
9
+ import { disableSchedule, updateSchedule, upsertSchedule, } from '../../api/scheduler/schedules.js';
10
+ import { DEFAULT_SERVICE_URLS } from '../../api/constants.js';
11
+ const SCHEDULE_PRESETS = [
12
+ { id: 'every-minute', label: 'Every minute', cron: '* * * * *' },
13
+ { id: 'hourly', label: 'Every hour', cron: '0 * * * *' },
14
+ { id: 'daily', label: 'Every day', cron: '0 0 * * *' },
15
+ ];
16
+ const DEFAULT_CRON_EXPRESSION = '* * * * *';
17
+ const presetFromCron = (cronExpression) => {
18
+ const normalized = cronExpression.trim();
19
+ const preset = SCHEDULE_PRESETS.find(item => item.cron === normalized);
20
+ return preset ? preset.id : 'custom';
21
+ };
22
+ /**
23
+ * Self-contained schedule menu that lets a user pick a cron schedule for a
24
+ * notebook and persists it to the scheduler service.
25
+ *
26
+ * The component owns its loading state and renders a spinner in place of the
27
+ * anchor icon while a save request is in flight. The scheduler service URL is
28
+ * configurable through the `baseUrl` prop.
29
+ */
30
+ export const ScheduleMenu = ({ notebookUid, scheduleUid, token, baseUrl = DEFAULT_SERVICE_URLS.SCHEDULER, enabled, icon = ClockIcon, iconColor, ariaLabel = 'Schedule execution', initialPreset = 'every-minute', initialCronExpression, onSaved, onDeleted, onError, }) => {
31
+ const providedInitialCron = (initialCronExpression || '').trim();
32
+ const hasProvidedInitialCron = providedInitialCron.length > 0;
33
+ const initialCron = hasProvidedInitialCron
34
+ ? providedInitialCron
35
+ : DEFAULT_CRON_EXPRESSION;
36
+ const [cronExpression, setCronExpression] = useState(initialCron);
37
+ const [hasExplicitCron, setHasExplicitCron] = useState(hasProvidedInitialCron);
38
+ const [isSaving, setIsSaving] = useState(false);
39
+ const [isDeleting, setIsDeleting] = useState(false);
40
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
41
+ const [deleteConfirmUid, setDeleteConfirmUid] = useState('');
42
+ useEffect(() => {
43
+ const nextProvidedCron = (initialCronExpression || '').trim();
44
+ const nextHasExplicitCron = nextProvidedCron.length > 0;
45
+ setHasExplicitCron(nextHasExplicitCron);
46
+ setCronExpression(nextHasExplicitCron ? nextProvidedCron : DEFAULT_CRON_EXPRESSION);
47
+ }, [initialCronExpression, scheduleUid, notebookUid]);
48
+ const preset = useMemo(() => (hasExplicitCron ? presetFromCron(cronExpression) : 'custom'), [cronExpression, hasExplicitCron]);
49
+ const isEnabled = (enabled ?? true) &&
50
+ Boolean(token) &&
51
+ Boolean(notebookUid || scheduleUid) &&
52
+ !isSaving &&
53
+ !isDeleting;
54
+ const selectedPresetDescription = useMemo(() => {
55
+ if (!hasExplicitCron) {
56
+ return 'Not scheduled';
57
+ }
58
+ const selected = SCHEDULE_PRESETS.find(item => item.cron === cronExpression.trim());
59
+ if (selected) {
60
+ return `${selected.label} (${selected.cron})`;
61
+ }
62
+ return `Custom (${cronExpression || 'Cron expression not set'})`;
63
+ }, [cronExpression, hasExplicitCron]);
64
+ const saveSchedule = async (nextPreset, nextCron) => {
65
+ const cron = nextCron.trim();
66
+ if (!token || (!notebookUid && !scheduleUid) || !cron) {
67
+ return;
68
+ }
69
+ setIsSaving(true);
70
+ try {
71
+ let schedule;
72
+ if (scheduleUid) {
73
+ const response = await updateSchedule(token, scheduleUid, {
74
+ cronExpression: cron,
75
+ preset: nextPreset,
76
+ }, baseUrl);
77
+ schedule = response.schedule;
78
+ }
79
+ else {
80
+ const response = await upsertSchedule(token, {
81
+ notebookUid: notebookUid,
82
+ cronExpression: cron,
83
+ preset: nextPreset,
84
+ enabled: true,
85
+ }, baseUrl);
86
+ schedule = response.schedule;
87
+ }
88
+ onSaved?.(schedule);
89
+ }
90
+ catch (error) {
91
+ onError?.(error);
92
+ }
93
+ finally {
94
+ setIsSaving(false);
95
+ }
96
+ };
97
+ const selectPreset = (nextPreset) => {
98
+ const next = SCHEDULE_PRESETS.find(item => item.id === nextPreset);
99
+ if (!next) {
100
+ return;
101
+ }
102
+ setHasExplicitCron(true);
103
+ setCronExpression(next.cron);
104
+ void saveSchedule(nextPreset, next.cron);
105
+ };
106
+ const applyCustomCron = () => {
107
+ const value = cronExpression.trim();
108
+ const inferredPreset = presetFromCron(value);
109
+ void saveSchedule(inferredPreset, value);
110
+ };
111
+ const performDeleteSchedule = async () => {
112
+ if (!token || !scheduleUid) {
113
+ return;
114
+ }
115
+ setIsDeleting(true);
116
+ try {
117
+ const response = await disableSchedule(token, scheduleUid, baseUrl);
118
+ setIsDeleteDialogOpen(false);
119
+ setDeleteConfirmUid('');
120
+ onDeleted?.(response.schedule);
121
+ }
122
+ catch (error) {
123
+ onError?.(error);
124
+ }
125
+ finally {
126
+ setIsDeleting(false);
127
+ }
128
+ };
129
+ return (_jsxs(_Fragment, { children: [_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(IconButton, { icon: isSaving ? () => _jsx(Spinner, { size: "small" }) : icon, variant: "invisible", sx: iconColor ? { color: iconColor } : undefined, "aria-label": ariaLabel, title: isSaving
130
+ ? `${ariaLabel} (saving…)`
131
+ : isEnabled
132
+ ? ariaLabel
133
+ : `${ariaLabel} (disabled)` }) }), _jsxs(ActionMenu.Overlay, { width: "medium", sx: { minWidth: 300 }, children: [_jsx(Box, { sx: {
134
+ px: 3,
135
+ pt: 3,
136
+ pb: 2,
137
+ borderBottom: '1px solid',
138
+ borderColor: 'border.default',
139
+ }, children: _jsx(Text, { as: "p", sx: { fontWeight: 600 }, children: "Schedule" }) }), _jsx(ActionList, { selectionVariant: "single", showDividers: true, children: SCHEDULE_PRESETS.map(option => (_jsxs(ActionList.Item, { selected: preset === option.id, disabled: !isEnabled, onSelect: () => selectPreset(option.id), children: [option.label, _jsx(ActionList.Description, { variant: "block", children: option.cron })] }, option.id))) }), _jsxs(Box, { sx: { p: 3, borderTop: '1px solid', borderColor: 'border.default' }, children: [_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mb: 2 }, children: "Cron Expression" }), _jsx(TextInput, { value: cronExpression, onChange: event => {
140
+ const value = event.currentTarget.value;
141
+ setHasExplicitCron(value.trim().length > 0);
142
+ setCronExpression(value);
143
+ }, placeholder: "* * * * *", "aria-label": "Cron expression", block: true, disabled: !isEnabled }), _jsxs(Box, { sx: { mt: 2, display: 'flex', gap: 2, flexWrap: 'wrap' }, children: [_jsx(Button, { size: "small", onClick: applyCustomCron, disabled: !isEnabled, children: "Apply Cron" }), scheduleUid ? (_jsx(Button, { size: "small", variant: "danger", leadingVisual: TrashIcon, onClick: () => setIsDeleteDialogOpen(true), disabled: !isEnabled, children: "Delete Schedule" })) : null] }), _jsxs(Text, { as: "p", sx: { mt: 2, fontSize: 0, color: 'fg.muted' }, children: ["Current: ", selectedPresetDescription] })] })] })] }), isDeleteDialogOpen && scheduleUid ? (_jsx(Dialog, { title: "Delete schedule", onClose: () => {
144
+ if (isDeleting) {
145
+ return;
146
+ }
147
+ setIsDeleteDialogOpen(false);
148
+ setDeleteConfirmUid('');
149
+ }, width: "medium", children: _jsxs(Box, { sx: { display: 'grid', gap: 3 }, children: [_jsxs(Text, { children: ["This action will disable the schedule and remove planned runs. Type", ' ', _jsx(Text, { as: "span", sx: { fontWeight: 700 }, children: scheduleUid }), ' ', "to confirm."] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Schedule UID" }), _jsx(TextInput, { value: deleteConfirmUid, onChange: event => setDeleteConfirmUid(event.currentTarget.value), placeholder: scheduleUid, autoFocus: true })] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', gap: 2 }, children: [_jsx(Button, { variant: "default", onClick: () => {
150
+ setIsDeleteDialogOpen(false);
151
+ setDeleteConfirmUid('');
152
+ }, disabled: isDeleting, children: "Cancel" }), _jsx(Button, { variant: "danger", onClick: () => void performDeleteSchedule(), disabled: isDeleting || deleteConfirmUid !== scheduleUid, children: "Delete schedule" })] })] }) })) : null] }));
153
+ };
154
+ export default ScheduleMenu;
@@ -0,0 +1 @@
1
+ export * from './ScheduleMenu';
@@ -2,6 +2,4 @@
2
2
  * Copyright (c) 2023-2025 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- export * from './IAMTokenEdit.js';
6
- export * from './IAMTokenNew.js';
7
- export * from './IAMTokens.js';
5
+ export * from './ScheduleMenu.js';
@@ -0,0 +1,14 @@
1
+ type ILiveRelativeTimeProps = {
2
+ value?: Date | string | number;
3
+ refreshIntervalMs?: number;
4
+ fallback?: string;
5
+ showTooltip?: boolean;
6
+ };
7
+ /**
8
+ * Display a live-updating relative time label (e.g. "5m ago", "in 2m").
9
+ *
10
+ * When `showTooltip` is true, a Primer tooltip shows ISO + localized datetime
11
+ * details with timezone information.
12
+ */
13
+ export declare function LiveRelativeTime({ value, refreshIntervalMs, fallback, showTooltip, }: ILiveRelativeTimeProps): JSX.Element;
14
+ export {};
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2023-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ import { useEffect, useMemo, useState } from 'react';
7
+ import { Tooltip } from '@primer/react';
8
+ import { formatDateTimeDetails, formatRelativeTime } from '../../utils/index.js';
9
+ /**
10
+ * Display a live-updating relative time label (e.g. "5m ago", "in 2m").
11
+ *
12
+ * When `showTooltip` is true, a Primer tooltip shows ISO + localized datetime
13
+ * details with timezone information.
14
+ */
15
+ export function LiveRelativeTime({ value, refreshIntervalMs = 30_000, fallback = '—', showTooltip = true, }) {
16
+ const [now, setNow] = useState(() => new Date());
17
+ useEffect(() => {
18
+ const timer = window.setInterval(() => {
19
+ setNow(new Date());
20
+ }, refreshIntervalMs);
21
+ return () => window.clearInterval(timer);
22
+ }, [refreshIntervalMs]);
23
+ const label = useMemo(() => formatRelativeTime(value, now), [value, now]);
24
+ const tooltip = useMemo(() => formatDateTimeDetails(value), [value]);
25
+ const content = (_jsx("button", { type: "button", "aria-label": tooltip || 'Date and time details', style: {
26
+ border: 'none',
27
+ background: 'transparent',
28
+ padding: 0,
29
+ margin: 0,
30
+ font: 'inherit',
31
+ color: 'inherit',
32
+ cursor: 'inherit',
33
+ lineHeight: 'inherit',
34
+ }, children: label ?? fallback }));
35
+ if (!showTooltip || !tooltip) {
36
+ return content;
37
+ }
38
+ return _jsx(Tooltip, { text: tooltip, children: content });
39
+ }
@@ -0,0 +1 @@
1
+ export * from './LiveRelativeTime';
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from './LiveRelativeTime.js';
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from 'react';
2
+ export type TimelineStatus = 'done' | 'current' | 'pending' | 'failed' | 'neutral';
3
+ export type TimelineItem = {
4
+ id: string;
5
+ title: string;
6
+ timestamp?: string;
7
+ status?: TimelineStatus;
8
+ subtitle?: ReactNode;
9
+ };
10
+ export type TimelineProps = {
11
+ items: TimelineItem[];
12
+ renderTimestamp?: (value?: string) => ReactNode;
13
+ };
14
+ /**
15
+ * Horizontal metro-style timeline with connected markers and labels.
16
+ *
17
+ * The first item is rendered on the left. Pass items in the desired order
18
+ * (for example: newest to oldest).
19
+ */
20
+ export declare const Timeline: ({ items, renderTimestamp }: TimelineProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from '@primer/react';
3
+ const statusStyles = {
4
+ done: {
5
+ dotBackground: 'success.emphasis',
6
+ dotBorder: 'success.emphasis',
7
+ lineColor: 'success.muted',
8
+ titleColor: 'fg.default',
9
+ },
10
+ current: {
11
+ dotBackground: 'attention.emphasis',
12
+ dotBorder: 'attention.emphasis',
13
+ lineColor: 'attention.muted',
14
+ titleColor: 'fg.default',
15
+ },
16
+ pending: {
17
+ dotBackground: 'canvas.default',
18
+ dotBorder: 'border.default',
19
+ lineColor: 'border.default',
20
+ titleColor: 'fg.muted',
21
+ },
22
+ failed: {
23
+ dotBackground: 'danger.emphasis',
24
+ dotBorder: 'danger.emphasis',
25
+ lineColor: 'danger.muted',
26
+ titleColor: 'danger.fg',
27
+ },
28
+ neutral: {
29
+ dotBackground: 'accent.emphasis',
30
+ dotBorder: 'accent.emphasis',
31
+ lineColor: 'accent.muted',
32
+ titleColor: 'fg.default',
33
+ },
34
+ };
35
+ /**
36
+ * Horizontal metro-style timeline with connected markers and labels.
37
+ *
38
+ * The first item is rendered on the left. Pass items in the desired order
39
+ * (for example: newest to oldest).
40
+ */
41
+ export const Timeline = ({ items, renderTimestamp }) => {
42
+ if (!items.length) {
43
+ return null;
44
+ }
45
+ return (_jsx(Box, { sx: {
46
+ overflowX: 'auto',
47
+ pb: 1,
48
+ }, children: _jsx(Box, { sx: {
49
+ display: 'flex',
50
+ alignItems: 'stretch',
51
+ minWidth: 'max-content',
52
+ }, children: items.map((item, index) => {
53
+ const status = item.status || 'neutral';
54
+ const style = statusStyles[status];
55
+ const hasNext = index < items.length - 1;
56
+ return (_jsxs(Box, { sx: {
57
+ display: 'flex',
58
+ flexDirection: 'column',
59
+ minWidth: 220,
60
+ flex: hasNext ? '0 0 220px' : '0 0 auto',
61
+ pr: hasNext ? 0 : 1,
62
+ }, children: [_jsxs(Box, { sx: {
63
+ display: 'flex',
64
+ alignItems: 'center',
65
+ minHeight: 28,
66
+ mb: 2,
67
+ }, children: [_jsx(Box, { sx: {
68
+ width: 16,
69
+ height: 16,
70
+ borderRadius: '50%',
71
+ border: '2px solid',
72
+ borderColor: style.dotBorder,
73
+ bg: style.dotBackground,
74
+ boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.2)',
75
+ } }), hasNext ? (_jsx(Box, { sx: {
76
+ flex: 1,
77
+ height: 4,
78
+ ml: 2,
79
+ mr: 2,
80
+ borderRadius: 999,
81
+ bg: style.lineColor,
82
+ } })) : null] }), _jsx(Text, { as: "p", sx: { fontWeight: 600, color: style.titleColor, mb: 1 }, children: item.title }), _jsx(Text, { as: "p", sx: {
83
+ fontSize: 0,
84
+ color: 'fg.muted',
85
+ mb: item.subtitle ? 1 : 0,
86
+ }, children: renderTimestamp
87
+ ? renderTimestamp(item.timestamp)
88
+ : item.timestamp || 'n/a' }), item.subtitle ? (_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.subtle' }, children: item.subtitle })) : null] }, item.id));
89
+ }) }) }));
90
+ };
@@ -0,0 +1 @@
1
+ export * from './Timeline';
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from './Timeline.js';
@@ -758,7 +758,7 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
758
758
  principalKind?: "user" | "organization" | "team";
759
759
  }) => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
760
760
  useToken: (tokenId: string) => import("@tanstack/react-query").UseQueryResult<IIAMToken | null | undefined, Error>;
761
- useTokens: () => import("@tanstack/react-query").UseQueryResult<any, Error>;
761
+ useTokens: () => import("@tanstack/react-query").UseQueryResult<(IIAMToken | undefined)[], Error>;
762
762
  useCreateToken: () => import("@tanstack/react-query").UseMutationResult<any, Error, Omit<IIAMToken, "id" | "value">, unknown>;
763
763
  useUpdateToken: () => import("@tanstack/react-query").UseMutationResult<any, Error, IIAMToken, unknown>;
764
764
  useDeleteToken: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
@@ -2166,11 +2166,12 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2166
2166
  queryKey: queryKeys.tokens.all(),
2167
2167
  queryFn: async () => {
2168
2168
  const resp = await requestDatalayer({
2169
- url: `${configuration.iamRunUrl}/api/iam/v1/tokens`,
2169
+ url: `${configuration.iamRunUrl}/api/iam/v1/api-keys`,
2170
2170
  method: 'GET',
2171
2171
  });
2172
- if (resp.success && resp.tokens) {
2173
- const tokens = resp.tokens
2172
+ const tokenItems = asArray(resp?.apiKeys ?? resp?.api_keys ?? resp?.tokens);
2173
+ if (resp.success && tokenItems.length > 0) {
2174
+ const tokens = tokenItems
2174
2175
  .map((t) => {
2175
2176
  const token = toToken(t);
2176
2177
  if (token) {
@@ -2194,7 +2195,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2194
2195
  return useMutation({
2195
2196
  mutationFn: async (token) => {
2196
2197
  const resp = await requestDatalayer({
2197
- url: `${configuration.iamRunUrl}/api/iam/v1/tokens`,
2198
+ url: `${configuration.iamRunUrl}/api/iam/v1/api-keys`,
2198
2199
  method: 'POST',
2199
2200
  body: {
2200
2201
  ...token,
@@ -2202,8 +2203,9 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2202
2203
  },
2203
2204
  });
2204
2205
  // Transform the token in the response
2205
- if (resp.success && resp.token) {
2206
- resp.token = toToken(resp.token);
2206
+ const tokenPayload = resp?.apiKey ?? resp?.api_key ?? resp?.token;
2207
+ if (resp.success && tokenPayload) {
2208
+ resp.token = toToken(tokenPayload);
2207
2209
  }
2208
2210
  return resp;
2209
2211
  },
@@ -2515,11 +2517,12 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2515
2517
  queryKey: queryKeys.tokens.detail(tokenId),
2516
2518
  queryFn: async () => {
2517
2519
  const resp = await requestDatalayer({
2518
- url: `${configuration.iamRunUrl}/api/iam/v1/tokens/${tokenId}`,
2520
+ url: `${configuration.iamRunUrl}/api/iam/v1/api-keys/${tokenId}`,
2519
2521
  method: 'GET',
2520
2522
  });
2521
- if (resp.success && resp.token) {
2522
- return toToken(resp.token);
2523
+ const tokenPayload = resp?.apiKey ?? resp?.api_key ?? resp?.token;
2524
+ if (resp.success && tokenPayload) {
2525
+ return toToken(tokenPayload);
2523
2526
  }
2524
2527
  return null;
2525
2528
  },
@@ -2534,14 +2537,15 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2534
2537
  return useMutation({
2535
2538
  mutationFn: async (token) => {
2536
2539
  return requestDatalayer({
2537
- url: `${configuration.iamRunUrl}/api/iam/v1/tokens/${token.id}`,
2540
+ url: `${configuration.iamRunUrl}/api/iam/v1/api-keys/${token.id}`,
2538
2541
  method: 'PUT',
2539
2542
  body: { ...token },
2540
2543
  });
2541
2544
  },
2542
2545
  onSuccess: resp => {
2543
- if (resp.success && resp.token) {
2544
- const tok = toToken(resp.token);
2546
+ const tokenPayload = resp?.apiKey ?? resp?.api_key ?? resp?.token;
2547
+ if (resp.success && tokenPayload) {
2548
+ const tok = toToken(tokenPayload);
2545
2549
  if (tok) {
2546
2550
  queryClient.setQueryData(queryKeys.tokens.detail(tok.id), tok);
2547
2551
  }
@@ -2557,7 +2561,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2557
2561
  return useMutation({
2558
2562
  mutationFn: async (tokenId) => {
2559
2563
  return requestDatalayer({
2560
- url: `${configuration.iamRunUrl}/api/iam/v1/tokens/${tokenId}`,
2564
+ url: `${configuration.iamRunUrl}/api/iam/v1/api-keys/${tokenId}`,
2561
2565
  method: 'DELETE',
2562
2566
  });
2563
2567
  },
@@ -16,7 +16,7 @@ export type ProjectRuntimeEntry = {
16
16
  agentName?: string;
17
17
  /** Agent runtime status (running, starting, terminated, etc.). */
18
18
  agentStatus?: string;
19
- /** The agent spec ID used to create the runtime. */
19
+ /** The agentspec ID used to create the runtime. */
20
20
  agentSpecId?: string;
21
21
  };
22
22
  export type ProjectStoreState = {
@@ -20,7 +20,7 @@ export type ProjectData = {
20
20
  isPublic: boolean;
21
21
  /** Attached agent (runtime) pod name, if any */
22
22
  attachedAgentPodName?: string;
23
- /** Attached agent spec ID (e.g. 'data-acquisition'), if any */
23
+ /** Attached agentspec ID (e.g. 'data-acquisition'), if any */
24
24
  attachedAgentSpecId?: string;
25
25
  };
26
26
  /**
@@ -31,7 +31,7 @@ export type CreateProjectRequest = {
31
31
  name: string;
32
32
  /** Project description */
33
33
  description?: string;
34
- /** Agent spec to attach (creates agent on project creation) */
34
+ /** Agentspec to attach (creates agent on project creation) */
35
35
  agentSpecId?: string;
36
36
  };
37
37
  /**
@@ -1,5 +1,5 @@
1
1
  export declare const asToken: (s: any) => IIAMToken;
2
- export type IIAMTokenVariant = 'user_token';
2
+ export type IIAMTokenVariant = 'secret' | 'publishable' | 'restricted' | 'temporary';
3
3
  export type IIAMToken = {
4
4
  id: string;
5
5
  variant: IIAMTokenVariant;
@@ -66,7 +66,7 @@ export declare class ProjectDTO {
66
66
  get createdAt(): Date;
67
67
  /** Attached agent runtime pod name, if any. */
68
68
  get attachedAgentPodName(): string | undefined;
69
- /** Attached agent spec ID (e.g., 'data-acquisition'), if any. */
69
+ /** Attached agentspec ID (e.g., 'data-acquisition'), if any. */
70
70
  get attachedAgentSpecId(): string | undefined;
71
71
  /** Human-readable name of the attached agent runtime, if any. */
72
72
  get attachedAgentGivenName(): string | undefined;
@@ -69,7 +69,7 @@ export class ProjectDTO {
69
69
  this._checkDeleted();
70
70
  return this._data.attached_agent_pod_name_s || undefined;
71
71
  }
72
- /** Attached agent spec ID (e.g., 'data-acquisition'), if any. */
72
+ /** Attached agentspec ID (e.g., 'data-acquisition'), if any. */
73
73
  get attachedAgentSpecId() {
74
74
  this._checkDeleted();
75
75
  return this._data.attached_agent_spec_id_s || undefined;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { createStore } from 'zustand/vanilla';
6
6
  import { useStore } from 'zustand';
7
- import { configLogger } from '../../utils/logger.js';
7
+ import { configLogger } from '../../utils/Logger.js';
8
8
  let loadConfigurationFromServer = true;
9
9
  let initialConfiguration = {
10
10
  runUrl: 'https://prod1.datalayer.run',
@@ -6,6 +6,13 @@ export declare const timeSince: (date: Date) => string;
6
6
  * Examples: "just now", "15m ago", "3h ago", "2d ago", "4w ago", "1y ago".
7
7
  */
8
8
  export declare const formatRelativeTime: (value?: Date | string | number, now?: Date) => string | undefined;
9
+ /**
10
+ * Build a detailed datetime string suitable for tooltips.
11
+ *
12
+ * Example:
13
+ * "2026-06-08T16:15:32.531Z • Jun 8, 2026, 6:15:32 PM CEST (Europe/Paris)"
14
+ */
15
+ export declare const formatDateTimeDetails: (value?: Date | string | number) => string | undefined;
9
16
  /**
10
17
  * Format a duration in milliseconds into a human-readable string.
11
18
  *
package/lib/utils/Date.js CHANGED
@@ -76,24 +76,57 @@ export const formatRelativeTime = (value, now = new Date()) => {
76
76
  if (Number.isNaN(ts)) {
77
77
  return typeof value === 'string' ? value : undefined;
78
78
  }
79
- const diffMs = Math.max(0, now.getTime() - ts);
80
- const seconds = Math.floor(diffMs / 1000);
81
- if (seconds < 60)
82
- return 'just now';
83
- const minutes = Math.floor(seconds / 60);
79
+ const diffMs = ts - now.getTime();
80
+ const inFuture = diffMs > 0;
81
+ const absSeconds = Math.floor(Math.abs(diffMs) / 1000);
82
+ if (absSeconds < 30)
83
+ return inFuture ? 'in a few seconds' : 'just now';
84
+ const withDirection = (value, unit) => {
85
+ return inFuture ? `in ${value}${unit}` : `${value}${unit} ago`;
86
+ };
87
+ if (absSeconds < 60)
88
+ return withDirection(absSeconds, 's');
89
+ const minutes = Math.floor(absSeconds / 60);
84
90
  if (minutes < 60)
85
- return `${minutes}m ago`;
91
+ return withDirection(minutes, 'm');
86
92
  const hours = Math.floor(minutes / 60);
87
93
  if (hours < 24)
88
- return `${hours}h ago`;
94
+ return withDirection(hours, 'h');
89
95
  const days = Math.floor(hours / 24);
90
96
  if (days < 7)
91
- return `${days}d ago`;
97
+ return withDirection(days, 'd');
92
98
  const weeks = Math.floor(days / 7);
93
99
  if (weeks < 52)
94
- return `${weeks}w ago`;
100
+ return withDirection(weeks, 'w');
95
101
  const years = Math.floor(days / 365);
96
- return `${years}y ago`;
102
+ return withDirection(years, 'y');
103
+ };
104
+ /**
105
+ * Build a detailed datetime string suitable for tooltips.
106
+ *
107
+ * Example:
108
+ * "2026-06-08T16:15:32.531Z • Jun 8, 2026, 6:15:32 PM CEST (Europe/Paris)"
109
+ */
110
+ export const formatDateTimeDetails = (value) => {
111
+ if (value === undefined || value === null) {
112
+ return undefined;
113
+ }
114
+ const date = value instanceof Date
115
+ ? value
116
+ : typeof value === 'number'
117
+ ? new Date(value)
118
+ : new Date(value);
119
+ const ts = date.getTime();
120
+ if (Number.isNaN(ts)) {
121
+ return typeof value === 'string' ? value : undefined;
122
+ }
123
+ const iso = date.toISOString();
124
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'local';
125
+ const local = new Intl.DateTimeFormat(undefined, {
126
+ dateStyle: 'medium',
127
+ timeStyle: 'long',
128
+ }).format(date);
129
+ return `${iso} • ${local} (${zone})`;
97
130
  };
98
131
  /**
99
132
  * Format a duration in milliseconds into a human-readable string.
@@ -15,6 +15,7 @@ export * from './Ids';
15
15
  export * from './Jwt';
16
16
  export * from './Jupyter';
17
17
  export * from './Lazy';
18
+ export * from './Logger';
18
19
  export * from './Msc';
19
20
  export * from './Name';
20
21
  export * from './Notebook';
@@ -19,6 +19,7 @@ export * from './Ids.js';
19
19
  export * from './Jwt.js';
20
20
  export * from './Jupyter.js';
21
21
  export * from './Lazy.js';
22
+ export * from './Logger.js';
22
23
  export * from './Msc.js';
23
24
  export * from './Name.js';
24
25
  export * from './Notebook.js';
@@ -0,0 +1,8 @@
1
+ export type APIKeyEditProps = {
2
+ /** Route to navigate after delete. Defaults to '/settings/iam/api-keys'. */
3
+ apiKeysListRoute?: string;
4
+ /** Whether to render the local edit title header. Defaults to true. */
5
+ showTitle?: boolean;
6
+ };
7
+ export declare const APIKeyEdit: ({ apiKeysListRoute, showTitle, }?: APIKeyEditProps) => import("react/jsx-runtime").JSX.Element;
8
+ export default APIKeyEdit;