@datalayer/core 1.0.19 → 1.0.20

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.
@@ -16,7 +16,7 @@ export interface SignInSimpleProps {
16
16
  onSignIn: (token: string, handle: string) => void;
17
17
  /**
18
18
  * Called when the user authenticates with an API key.
19
- * If not provided the "Sign in with API Key" button is hidden.
19
+ * If not provided the "Sign In with an API Key" button is hidden.
20
20
  */
21
21
  onApiKeySignIn?: (apiKey: string) => void;
22
22
  /**
@@ -12,10 +12,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
12
12
  *
13
13
  * @module views/signin
14
14
  */
15
- import { useState, useCallback, useRef, useMemo } from 'react';
15
+ import { useState, useCallback, useRef, useMemo, useEffect, } from 'react';
16
16
  import { Box, Button, FormControl, Heading, Text, Textarea, TextInput, } from '@primer/react';
17
- import { Dialog } from '@primer/react/experimental';
18
- import { EyeIcon, EyeClosedIcon, KeyIcon, TelescopeIcon, } from '@primer/octicons-react';
17
+ import { EyeIcon, EyeClosedIcon, KeyIcon, LinkExternalIcon, TelescopeIcon, } from '@primer/octicons-react';
19
18
  import { coreStore } from '../../state/index.js';
20
19
  // ── Component ────────────────────────────────────────────────────────
21
20
  export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp, title = 'Datalayer OTEL', description = 'Sign in to access the observability dashboard.', leadingIcon = _jsx(TelescopeIcon, { size: 24 }), }) => {
@@ -34,6 +33,24 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
34
33
  const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
35
34
  const [apiKey, setApiKey] = useState('');
36
35
  const apiKeyRef = useRef(null);
36
+ useEffect(() => {
37
+ if (showApiKeyDialog) {
38
+ apiKeyRef.current?.focus();
39
+ }
40
+ }, [showApiKeyDialog]);
41
+ const closeApiKeyDialog = useCallback(() => {
42
+ setShowApiKeyDialog(false);
43
+ setApiKey('');
44
+ }, []);
45
+ const handleApiKeyAuthenticate = useCallback(() => {
46
+ if (!apiKey.trim() || !onApiKeySignIn)
47
+ return;
48
+ onApiKeySignIn(apiKey.trim());
49
+ closeApiKeyDialog();
50
+ }, [apiKey, onApiKeySignIn, closeApiKeyDialog]);
51
+ const handleSignUp = useCallback(() => {
52
+ window.open('https://datalayer.ai/signup', '_blank', 'noopener,noreferrer');
53
+ }, []);
37
54
  const submit = useCallback(async () => {
38
55
  if (!handle || !password || loading)
39
56
  return;
@@ -87,34 +104,41 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
87
104
  gap: 2,
88
105
  mb: 4,
89
106
  justifyContent: 'center',
90
- }, children: [leadingIcon, _jsx(Heading, { sx: { fontSize: 3 }, children: title })] }), _jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', mb: 3, textAlign: 'center' }, children: description }), _jsxs(FormControl, { required: true, sx: { mb: 3 }, children: [_jsx(FormControl.Label, { children: "Username" }), _jsx(TextInput, { autoFocus: true, block: true, placeholder: "Your username", value: handle, onChange: e => setHandle(e.target.value), onKeyDown: handleKeyDown })] }), _jsxs(FormControl, { required: true, sx: { mb: 3 }, children: [_jsx(FormControl.Label, { children: "Password" }), _jsx(TextInput, { block: true, placeholder: "Your password", type: showPassword ? 'text' : 'password', value: password, onChange: e => setPassword(e.target.value), onKeyDown: handleKeyDown, trailingAction: _jsx(TextInput.Action, { onClick: () => setShowPassword(!showPassword), icon: showPassword ? EyeClosedIcon : EyeIcon, "aria-label": showPassword ? 'Hide password' : 'Show password', sx: { color: 'var(--fgColor-muted)' } }) })] }), error && (_jsx(Text, { sx: { color: 'danger.fg', fontSize: 1, mb: 3, display: 'block' }, children: error })), _jsx(Button, { variant: "primary", block: true, disabled: loading || !handle || !password, onClick: submit, children: loading ? 'Signing in…' : 'Sign in' }), onApiKeySignIn && (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: {
107
+ }, children: [leadingIcon, _jsx(Heading, { sx: { fontSize: 3 }, children: title })] }), _jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', mb: 3, textAlign: 'center' }, children: description }), _jsxs(FormControl, { required: true, sx: { mb: 3 }, children: [_jsx(FormControl.Label, { children: "Username" }), _jsx(TextInput, { autoFocus: true, block: true, placeholder: "Your username", value: handle, onChange: e => setHandle(e.target.value), onKeyDown: handleKeyDown })] }), _jsxs(FormControl, { required: true, sx: { mb: 3 }, children: [_jsx(FormControl.Label, { children: "Password" }), _jsx(TextInput, { block: true, placeholder: "Your password", type: showPassword ? 'text' : 'password', value: password, onChange: e => setPassword(e.target.value), onKeyDown: handleKeyDown, trailingAction: _jsx(TextInput.Action, { onClick: () => setShowPassword(!showPassword), icon: showPassword ? EyeClosedIcon : EyeIcon, "aria-label": showPassword ? 'Hide password' : 'Show password', sx: { color: 'var(--fgColor-muted)' } }) })] }), error && (_jsx(Text, { sx: { color: 'danger.fg', fontSize: 1, mb: 3, display: 'block' }, children: error })), _jsx(Button, { variant: "primary", block: true, disabled: loading || !handle || !password, onClick: submit, children: loading ? 'Signing in…' : 'Sign in' }), _jsx(Box, { sx: { mt: 2, display: 'flex', justifyContent: 'center' }, children: _jsx(Button, { size: "small", variant: "invisible", leadingVisual: LinkExternalIcon, onClick: handleSignUp, children: "Sign Up" }) }), onApiKeySignIn && (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: {
91
108
  display: 'flex',
92
109
  alignItems: 'center',
93
110
  gap: 2,
94
111
  my: 3,
95
- }, children: [_jsx(Box, { sx: { flex: 1, height: '1px', bg: 'border.default' } }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "or" }), _jsx(Box, { sx: { flex: 1, height: '1px', bg: 'border.default' } })] }), _jsx(Button, { block: true, leadingVisual: KeyIcon, onClick: () => setShowApiKeyDialog(true), children: "Sign in with API Key" }), showApiKeyDialog && (_jsx(Dialog, { title: "Enter your API Key", onClose: () => {
96
- setShowApiKeyDialog(false);
97
- setApiKey('');
98
- }, initialFocusRef: apiKeyRef, footerButtons: [
99
- {
100
- buttonType: 'default',
101
- onClick: () => {
102
- setShowApiKeyDialog(false);
103
- setApiKey('');
104
- },
105
- content: 'Cancel',
106
- },
107
- {
108
- buttonType: 'primary',
109
- onClick: () => {
110
- if (apiKey.trim()) {
111
- onApiKeySignIn(apiKey.trim());
112
- setShowApiKeyDialog(false);
113
- setApiKey('');
114
- }
115
- },
116
- content: 'Authenticate',
117
- },
118
- ], children: _jsx(Box, { as: "form", children: _jsxs(FormControl, { required: true, children: [_jsx(FormControl.Label, { children: "API Key" }), _jsx(Textarea, { block: true, required: true, autoFocus: true, placeholder: "Paste your API key here", value: apiKey, onInput: (e) => setApiKey(e.target.value), ref: apiKeyRef })] }) }) }))] }))] }) }));
112
+ }, children: [_jsx(Box, { sx: { flex: 1, height: '1px', bg: 'border.default' } }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "or" }), _jsx(Box, { sx: { flex: 1, height: '1px', bg: 'border.default' } })] }), _jsx(Button, { block: true, leadingVisual: KeyIcon, onClick: () => setShowApiKeyDialog(true), children: "Sign In with an API Key" }), showApiKeyDialog && (_jsx(Box, { role: "dialog", "aria-modal": "true", "aria-labelledby": "signin-api-key-title", onKeyDown: event => {
113
+ if (event.key === 'Escape') {
114
+ closeApiKeyDialog();
115
+ }
116
+ }, onClick: closeApiKeyDialog, sx: {
117
+ position: 'fixed',
118
+ inset: 0,
119
+ zIndex: 200,
120
+ p: 3,
121
+ bg: 'canvas.backdrop',
122
+ display: 'flex',
123
+ alignItems: 'center',
124
+ justifyContent: 'center',
125
+ }, children: _jsxs(Box, { as: "form", onClick: event => event.stopPropagation(), onSubmit: event => {
126
+ event.preventDefault();
127
+ handleApiKeyAuthenticate();
128
+ }, sx: {
129
+ width: 'min(560px, 100%)',
130
+ p: 3,
131
+ borderRadius: 2,
132
+ border: '1px solid',
133
+ borderColor: 'border.default',
134
+ bg: 'canvas.overlay',
135
+ color: 'fg.default',
136
+ boxShadow: 'shadow.large',
137
+ }, children: [_jsx(Heading, { id: "signin-api-key-title", sx: { fontSize: 2, mb: 2 }, children: "Enter your API Key" }), _jsxs(FormControl, { required: true, children: [_jsx(FormControl.Label, { children: "API Key" }), _jsx(Textarea, { block: true, required: true, autoFocus: true, placeholder: "Paste your API key here", value: apiKey, onInput: (e) => setApiKey(e.target.value), ref: apiKeyRef })] }), _jsxs(Box, { sx: {
138
+ mt: 3,
139
+ display: 'flex',
140
+ justifyContent: 'flex-end',
141
+ gap: 2,
142
+ }, children: [_jsx(Button, { type: "button", onClick: closeApiKeyDialog, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", disabled: !apiKey.trim(), children: "Authenticate" })] })] }) }))] }))] }) }));
119
143
  };
120
144
  export default SignInSimple;
@@ -15,6 +15,22 @@ export interface UserBadgeProps {
15
15
  token: string;
16
16
  /** Display variant for the claims overlay. */
17
17
  variant?: 'full' | 'small';
18
+ /**
19
+ * When the popover is rendered in the ``small`` variant, show a ``+``
20
+ * affordance in the top-right corner of the header that lets the user
21
+ * expand the overlay to the ``full`` variant (raw JSON claims).
22
+ *
23
+ * Defaults to ``true``. Ignored when ``variant`` is already ``full``.
24
+ */
25
+ showExpandToggle?: boolean;
26
+ /**
27
+ * Threshold (in milliseconds) below which the expiration timestamp is
28
+ * rendered in ``attention.fg`` (yellow) to warn that the token is about
29
+ * to expire. Tokens already past ``exp`` render in ``danger.fg`` (red);
30
+ * tokens further out than this threshold render in ``success.fg``
31
+ * (green). Defaults to ``3_600_000`` (1 hour).
32
+ */
33
+ expirationWarningMs?: number;
18
34
  }
19
35
  export declare const UserBadge: React.FC<UserBadgeProps>;
20
36
  export default UserBadge;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /*
3
3
  * Copyright (c) 2023-2025 Datalayer, Inc.
4
4
  * Distributed under the terms of the Modified BSD License.
@@ -17,9 +17,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
17
17
  import { useState, useRef, useCallback } from 'react';
18
18
  import { Box, Text } from '@primer/react';
19
19
  import { parseJwtPayload, getDatalayerJwtUser, getDatalayerDisplayName, } from '../../utils/Jwt.js';
20
+ import { getRelativeTime } from '../../utils/Date.js';
20
21
  // ── Component ─────────────────────────────────────────────────────
21
- export const UserBadge = ({ token, variant = 'full', }) => {
22
+ export const UserBadge = ({ token, variant = 'full', showExpandToggle = true, expirationWarningMs = 60 * 60 * 1000, }) => {
22
23
  const [open, setOpen] = useState(false);
24
+ const [expanded, setExpanded] = useState(false);
23
25
  const closeTimer = useRef(null);
24
26
  const handleEnter = useCallback(() => {
25
27
  if (closeTimer.current)
@@ -34,9 +36,25 @@ export const UserBadge = ({ token, variant = 'full', }) => {
34
36
  const user = getDatalayerJwtUser(token);
35
37
  const displayName = getDatalayerDisplayName(user, user?.handle ?? '');
36
38
  const claims = parseJwtPayload(token);
39
+ // Colour the trigger label based on token expiry:
40
+ // - red when already expired,
41
+ // - yellow when expiring within ``expirationWarningMs``,
42
+ // - muted grey otherwise.
43
+ const msUntilExpiry = claims?.exp
44
+ ? claims.exp * 1000 - Date.now()
45
+ : undefined;
46
+ const expired = msUntilExpiry !== undefined && msUntilExpiry <= 0;
47
+ const expiringSoon = msUntilExpiry !== undefined &&
48
+ msUntilExpiry > 0 &&
49
+ msUntilExpiry <= expirationWarningMs;
50
+ const triggerColor = expired
51
+ ? 'danger.fg'
52
+ : expiringSoon
53
+ ? 'attention.fg'
54
+ : 'fg.muted';
37
55
  return (_jsxs(Box, { sx: { position: 'relative' }, onMouseEnter: handleEnter, onMouseLeave: handleLeave, children: [_jsx(Text, { sx: {
38
56
  fontSize: 1,
39
- color: 'fg.muted',
57
+ color: triggerColor,
40
58
  cursor: 'default',
41
59
  borderBottom: '1px dashed',
42
60
  borderColor: 'border.muted',
@@ -54,13 +72,36 @@ export const UserBadge = ({ token, variant = 'full', }) => {
54
72
  borderRadius: 2,
55
73
  boxShadow: 'shadow.large',
56
74
  overflow: 'hidden',
57
- }, children: [_jsx(Box, { sx: {
75
+ }, children: [_jsxs(Box, { sx: {
58
76
  px: 3,
59
77
  py: 2,
60
78
  bg: 'canvas.subtle',
61
79
  borderBottom: '1px solid',
62
80
  borderColor: 'border.default',
63
- }, children: _jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children: "JWT Claims" }) }), claims.user && (_jsx(Box, { sx: {
81
+ display: 'flex',
82
+ alignItems: 'center',
83
+ justifyContent: 'space-between',
84
+ gap: 2,
85
+ }, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children: "JWT Claims" }), variant === 'small' && showExpandToggle && (_jsx(Box, { as: "button", type: "button", onClick: () => setExpanded(v => !v), "aria-label": expanded ? 'Hide full JWT claims' : 'Show full JWT claims', "aria-expanded": expanded, title: expanded ? 'Hide full JWT claims' : 'Show full JWT claims', sx: {
86
+ appearance: 'none',
87
+ border: '1px solid',
88
+ borderColor: 'border.default',
89
+ borderRadius: 1,
90
+ bg: 'canvas.default',
91
+ color: 'fg.default',
92
+ fontSize: 1,
93
+ lineHeight: 1,
94
+ width: '20px',
95
+ height: '20px',
96
+ display: 'inline-flex',
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ cursor: 'pointer',
100
+ p: 0,
101
+ '&:hover': {
102
+ bg: 'canvas.subtle',
103
+ },
104
+ }, children: expanded ? '−' : '+' }))] }), claims.user && (_jsx(Box, { sx: {
64
105
  px: 3,
65
106
  py: 2,
66
107
  borderBottom: '1px solid',
@@ -70,7 +111,49 @@ export const UserBadge = ({ token, variant = 'full', }) => {
70
111
  gridTemplateColumns: '90px 1fr',
71
112
  gap: 1,
72
113
  fontSize: 0,
73
- }, children: [claims.user.email && (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { color: 'fg.muted' }, children: "Email" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.email })] })), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Handle" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.handle }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "UID" }), _jsx(Text, { sx: { fontFamily: 'mono', wordBreak: 'break-all' }, children: claims.user.uid }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Issued" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: new Date(claims.iat * 1000).toISOString() }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Expires" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: new Date(claims.exp * 1000).toISOString() })] }) })), (claims.user?.roles ?? claims.roles ?? []).length > 0 && (_jsxs(Box, { sx: {
114
+ }, children: [claims.user.email && (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { color: 'fg.muted' }, children: "Email" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.email })] })), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Handle" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.handle }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "UID" }), _jsx(Text, { sx: { fontFamily: 'mono', wordBreak: 'break-all' }, children: claims.user.uid }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Issued" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: new Date(claims.iat * 1000).toISOString() }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Expires" }), _jsxs(Box, { sx: {
115
+ display: 'flex',
116
+ alignItems: 'center',
117
+ gap: 2,
118
+ minWidth: 0,
119
+ }, children: [_jsx(Text, { sx: {
120
+ fontFamily: 'mono',
121
+ overflow: 'hidden',
122
+ textOverflow: 'ellipsis',
123
+ whiteSpace: 'nowrap',
124
+ }, children: new Date(claims.exp * 1000).toISOString() }), (() => {
125
+ const expDate = new Date(claims.exp * 1000);
126
+ const msUntilExpiry = expDate.getTime() - Date.now();
127
+ const expired = msUntilExpiry <= 0;
128
+ const warning = !expired && msUntilExpiry <= expirationWarningMs;
129
+ const bg = expired
130
+ ? 'danger.subtle'
131
+ : warning
132
+ ? 'attention.subtle'
133
+ : 'success.subtle';
134
+ const fg = expired
135
+ ? 'danger.fg'
136
+ : warning
137
+ ? 'attention.fg'
138
+ : 'success.fg';
139
+ const border = expired
140
+ ? 'danger.muted'
141
+ : warning
142
+ ? 'attention.muted'
143
+ : 'success.muted';
144
+ return (_jsx(Box, { sx: {
145
+ flexShrink: 0,
146
+ fontSize: 0,
147
+ px: 1,
148
+ py: '1px',
149
+ bg,
150
+ color: fg,
151
+ borderRadius: 1,
152
+ border: '1px solid',
153
+ borderColor: border,
154
+ whiteSpace: 'nowrap',
155
+ }, children: getRelativeTime(expDate) }));
156
+ })()] })] }) })), (claims.user?.roles ?? claims.roles ?? []).length > 0 && (_jsxs(Box, { sx: {
74
157
  px: 3,
75
158
  py: 2,
76
159
  borderBottom: '1px solid',
@@ -85,7 +168,7 @@ export const UserBadge = ({ token, variant = 'full', }) => {
85
168
  borderRadius: 1,
86
169
  border: '1px solid',
87
170
  borderColor: 'accent.muted',
88
- }, children: r }, r))) })] })), variant === 'full' && (_jsx(Box, { as: "pre", sx: {
171
+ }, children: r }, r))) })] })), (variant === 'full' || expanded) && (_jsx(Box, { as: "pre", sx: {
89
172
  m: 0,
90
173
  px: 3,
91
174
  py: 2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/core",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",