@plutonhq/core-frontend 0.1.20 → 0.1.22

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 (27) hide show
  1. package/dist-lib/components/Storage/AddStorage/AddStorage.module.scss.js +42 -26
  2. package/dist-lib/components/Storage/AddStorage/AddStorage.module.scss.js.map +1 -1
  3. package/dist-lib/components/Storage/StorageAuthSettings/StorageAuthSettings.d.ts.map +1 -1
  4. package/dist-lib/components/Storage/StorageAuthSettings/StorageAuthSettings.js +119 -53
  5. package/dist-lib/components/Storage/StorageAuthSettings/StorageAuthSettings.js.map +1 -1
  6. package/dist-lib/components/Storage/StorageItem/StorageItem.d.ts.map +1 -1
  7. package/dist-lib/components/Storage/StorageItem/StorageItem.js +1 -1
  8. package/dist-lib/components/Storage/StorageItem/StorageItem.js.map +1 -1
  9. package/dist-lib/components/common/form/StoragePicker/StoragePicker.d.ts +1 -0
  10. package/dist-lib/components/common/form/StoragePicker/StoragePicker.d.ts.map +1 -1
  11. package/dist-lib/components/common/form/StoragePicker/StoragePicker.js +57 -51
  12. package/dist-lib/components/common/form/StoragePicker/StoragePicker.js.map +1 -1
  13. package/dist-lib/components/common/form/StoragePicker/StoragePicker.module.scss.js +8 -4
  14. package/dist-lib/components/common/form/StoragePicker/StoragePicker.module.scss.js.map +1 -1
  15. package/dist-lib/services/storage.d.ts +10 -0
  16. package/dist-lib/services/storage.d.ts.map +1 -1
  17. package/dist-lib/services/storage.js +55 -20
  18. package/dist-lib/services/storage.js.map +1 -1
  19. package/dist-lib/services.js +55 -52
  20. package/dist-lib/styles/core-frontend.css +1 -1
  21. package/package.json +1 -1
  22. package/src/components/Storage/AddStorage/AddStorage.module.scss +74 -0
  23. package/src/components/Storage/StorageAuthSettings/StorageAuthSettings.tsx +136 -3
  24. package/src/components/Storage/StorageItem/StorageItem.tsx +0 -1
  25. package/src/components/common/form/StoragePicker/StoragePicker.module.scss +27 -0
  26. package/src/components/common/form/StoragePicker/StoragePicker.tsx +19 -5
  27. package/src/services/storage.ts +49 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutonhq/core-frontend",
3
3
  "description": "Pluton Core Frontend Library",
4
- "version": "0.1.20",
4
+ "version": "0.1.22",
5
5
  "author": "Plutonhq",
6
6
  "license": "Apache-2.0",
7
7
  "publishConfig": {
@@ -166,3 +166,77 @@
166
166
  }
167
167
  }
168
168
  }
169
+
170
+ .oauthContainer {
171
+ background-color: var(--background-color);
172
+ border: 1px solid var(--line-color);
173
+ border-radius: 6px;
174
+ box-sizing: border-box;
175
+ padding: 12px;
176
+ word-break: break-all;
177
+ &.success {
178
+ background-color: var(--success-bg-color);
179
+ border-color: var(--success-bg-color);
180
+ color: var(--success-text-color);
181
+ }
182
+ &.error {
183
+ background-color: var(--error-bg-color);
184
+ border-color: var(--error-bg-color);
185
+ color: var(--error-text-color);
186
+ }
187
+ }
188
+
189
+ .oauthButton {
190
+ display: flex;
191
+ width: 100%;
192
+ flex-direction: column;
193
+ align-items: flex-end;
194
+ .oauthAuthorizeBtn {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 8px;
198
+ padding: 10px 18px;
199
+ font-size: 13px;
200
+ font-weight: 600;
201
+ color: #fff;
202
+ background-color: var(--primary-color);
203
+ border: none;
204
+ border-radius: 6px;
205
+ cursor: pointer;
206
+ transition: background-color 0.2s;
207
+ &:hover {
208
+ opacity: 0.9;
209
+ }
210
+ }
211
+ }
212
+
213
+ .oauthInnerBtn {
214
+ display: inline-flex;
215
+ align-items: center;
216
+ gap: 6px;
217
+ padding: 6px 14px;
218
+ font-size: 12px;
219
+ font-weight: 500;
220
+ color: var(--content-text-color);
221
+ background-color: var(--content-background-color);
222
+ border: none;
223
+ border-radius: 6px;
224
+ cursor: pointer;
225
+ margin-top: 8px;
226
+ &:hover {
227
+ background-color: var(--primary-color);
228
+ color: var(--primary-text-color);
229
+ }
230
+ }
231
+
232
+ .oauthProgress {
233
+ p {
234
+ margin: 6px 0;
235
+ font-size: 13px;
236
+ line-height: 1.5;
237
+ a {
238
+ color: var(--primary-color);
239
+ text-decoration: underline;
240
+ }
241
+ }
242
+ }
@@ -1,9 +1,10 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { storageOptionField } from '../../../@types/storages';
3
3
  import Tristate from '../../common/form/Tristate/Tristate';
4
4
  import Icon from '../../common/Icon/Icon';
5
5
  import classes from '../AddStorage/AddStorage.module.scss';
6
6
  import StorageSettings from '../StorageSettings/StorageSettings';
7
+ import { startStorageAuthorize, getStorageAuthorizeStatus, cancelStorageAuthorize } from '../../../services/storage';
7
8
 
8
9
  interface StorageAuthSettingsProps {
9
10
  storageType: string;
@@ -16,6 +17,8 @@ interface StorageAuthSettingsProps {
16
17
  onAuthTypeChange: (authType: string) => void;
17
18
  }
18
19
 
20
+ type OAuthStatus = 'idle' | 'authorizing' | 'success' | 'error';
21
+
19
22
  const StorageAuthSettings = ({
20
23
  storageType,
21
24
  fields,
@@ -29,6 +32,16 @@ const StorageAuthSettings = ({
29
32
  const [showAdvanced, setShowAdvanced] = useState(true);
30
33
  const [showOAuthDoc, setShowOAuthDoc] = useState(false);
31
34
 
35
+ // OAuth auto-authorize state
36
+ const [oauthStatus, setOauthStatus] = useState<OAuthStatus>('idle');
37
+ const [authSessionId, setAuthSessionId] = useState<string | null>(null);
38
+ const [authUrl, setAuthUrl] = useState<string | null>(null);
39
+ const [authError, setAuthError] = useState<string | null>(null);
40
+ const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
41
+
42
+ const installType = (window as any).plutonInstallType;
43
+ const isDesktop = installType === 'binary' || installType === 'dev';
44
+
32
45
  console.log('availableAuthTypes :', fields, authTypes, currentAuthType);
33
46
 
34
47
  useEffect(() => {
@@ -37,6 +50,76 @@ const StorageAuthSettings = ({
37
50
  }
38
51
  }, [authTypes, currentAuthType, onAuthTypeChange]);
39
52
 
53
+ // Cleanup polling on unmount
54
+ useEffect(() => {
55
+ return () => {
56
+ if (pollingRef.current) {
57
+ clearInterval(pollingRef.current);
58
+ }
59
+ };
60
+ }, []);
61
+
62
+ const stopPolling = useCallback(() => {
63
+ if (pollingRef.current) {
64
+ clearInterval(pollingRef.current);
65
+ pollingRef.current = null;
66
+ }
67
+ }, []);
68
+
69
+ const startOAuthAuthorize = useCallback(async () => {
70
+ setOauthStatus('authorizing');
71
+ setAuthUrl(null);
72
+ setAuthError(null);
73
+
74
+ try {
75
+ const { sessionId } = await startStorageAuthorize(storageType);
76
+ setAuthSessionId(sessionId);
77
+
78
+ // Start polling every 2 seconds
79
+ pollingRef.current = setInterval(async () => {
80
+ try {
81
+ const result = await getStorageAuthorizeStatus(sessionId);
82
+
83
+ if (result.authUrl) {
84
+ setAuthUrl(result.authUrl);
85
+ }
86
+
87
+ if (result.status === 'success' && result.token) {
88
+ stopPolling();
89
+ setOauthStatus('success');
90
+ onUpdate({ ...settings, token: result.token });
91
+ } else if (result.status === 'error') {
92
+ stopPolling();
93
+ setOauthStatus('error');
94
+ setAuthError(result.error || 'Authorization failed');
95
+ }
96
+ } catch (err: any) {
97
+ stopPolling();
98
+ setOauthStatus('error');
99
+ setAuthError(err?.message || 'Failed to check authorization status');
100
+ }
101
+ }, 2000);
102
+ } catch (err: any) {
103
+ setOauthStatus('error');
104
+ setAuthError(err?.message || 'Failed to start authorization');
105
+ }
106
+ }, [storageType, settings, onUpdate, stopPolling]);
107
+
108
+ const handleCancelAuthorize = useCallback(async () => {
109
+ stopPolling();
110
+ if (authSessionId) {
111
+ try {
112
+ await cancelStorageAuthorize(authSessionId);
113
+ } catch {
114
+ // Ignore cancel errors
115
+ }
116
+ }
117
+ setOauthStatus('idle');
118
+ setAuthSessionId(null);
119
+ setAuthUrl(null);
120
+ setAuthError(null);
121
+ }, [authSessionId, stopPolling]);
122
+
40
123
  return (
41
124
  <div className={classes.authSettings}>
42
125
  <div
@@ -67,14 +150,64 @@ const StorageAuthSettings = ({
67
150
  />
68
151
  </div>
69
152
  )}
70
-
71
153
  <StorageSettings
72
154
  fields={fields.filter((f) => f.authFieldType === currentAuthType)}
73
155
  settings={settings}
74
156
  onUpdate={(newSettings) => onUpdate(newSettings)}
75
157
  errors={errors}
76
158
  />
77
- {currentAuthType === 'oauth' && (
159
+
160
+ {currentAuthType === 'oauth' && isDesktop && oauthStatus === 'idle' && (
161
+ <div className={classes.oauthButton}>
162
+ <button className={classes.oauthAuthorizeBtn} onClick={startOAuthAuthorize}>
163
+ <Icon type={'key'} size={14} /> Authorize & Get Access Token
164
+ </button>
165
+ </div>
166
+ )}
167
+
168
+ {currentAuthType === 'oauth' && isDesktop && oauthStatus !== 'idle' && (
169
+ <div className={`${classes.oauthContainer} ${classes[oauthStatus]}`}>
170
+ {oauthStatus === 'authorizing' && (
171
+ <div className={classes.oauthProgress}>
172
+ <p>
173
+ <strong>Waiting for authorization...</strong>
174
+ <br />A browser window should have opened. Please authorize the connection in your browser.
175
+ </p>
176
+ {authUrl && (
177
+ <p>
178
+ If the browser didn't open automatically,{' '}
179
+ <a href={authUrl} target="_blank" rel="noopener noreferrer">
180
+ click here to authorize
181
+ </a>
182
+ .
183
+ </p>
184
+ )}
185
+ <button className={classes.oauthInnerBtn} onClick={handleCancelAuthorize}>
186
+ Cancel
187
+ </button>
188
+ </div>
189
+ )}
190
+ {oauthStatus === 'success' && (
191
+ <div className={classes.oauthProgress}>
192
+ <p>
193
+ <Icon type={'check'} size={14} /> <strong>Authorization successful!</strong> Token has been automatically filled in.
194
+ </p>
195
+ </div>
196
+ )}
197
+ {oauthStatus === 'error' && (
198
+ <div className={classes.oauthProgress}>
199
+ <p>
200
+ <strong>Authorization failed:</strong> {authError}
201
+ </p>
202
+ <button className={classes.oauthInnerBtn} onClick={startOAuthAuthorize}>
203
+ <Icon type={'key'} size={14} /> Try Again
204
+ </button>
205
+ </div>
206
+ )}
207
+ </div>
208
+ )}
209
+
210
+ {currentAuthType === 'oauth' && !isDesktop && (
78
211
  <div className={classes.oauthDoc}>
79
212
  <h4 onClick={() => setShowOAuthDoc(!showOAuthDoc)}>
80
213
  <Icon type={'key'} size={14} /> Acquiring the OAuth Access Token
@@ -34,7 +34,6 @@ const StorageItem = ({ storage, layout }: StorageItemProps) => {
34
34
  onSuccess: (data: any) => {
35
35
  console.log('Success :', data);
36
36
  toast.success(`Removed Storage Successfully!`, { autoClose: 5000 });
37
- close();
38
37
  },
39
38
  });
40
39
  };
@@ -24,6 +24,9 @@
24
24
  width: calc(65% - 10px);
25
25
  margin-left: 10px;
26
26
  position: relative;
27
+ display: flex;
28
+ flex-direction: row;
29
+ align-items: center;
27
30
  .fileManagerBtn {
28
31
  position: absolute;
29
32
  right: 1px;
@@ -35,9 +38,33 @@
35
38
  color: var(--primary-color);
36
39
  }
37
40
  }
41
+ .defaultPath {
42
+ display: block;
43
+ min-width: fit-content;
44
+ background: var(--background-color);
45
+ color: var(--content-text-color-light);
46
+ cursor: default;
47
+ padding: 8px;
48
+ padding-top: 9px;
49
+ border-radius: 6px 0 0 6px;
50
+ border: 1px solid var(--field-line-color);
51
+ border-right: 0;
52
+ box-sizing: border-box;
53
+ white-space: nowrap;
54
+ overflow: hidden;
55
+ text-overflow: ellipsis;
56
+ }
57
+ div[class*='_inputField_'] {
58
+ display: inline-block;
59
+ }
38
60
  div[class*='_inputField_'] input {
39
61
  padding: 10px 12px;
40
62
  }
63
+ &.withBucket {
64
+ div[class*='_inputField_'] input {
65
+ border-radius: 0 6px 6px 0;
66
+ }
67
+ }
41
68
  }
42
69
  & > div {
43
70
  border-radius: 4px;
@@ -7,7 +7,7 @@ import FolderPicker from '../../FolderPicker/FolderPicker';
7
7
  import Icon from '../../Icon/Icon';
8
8
  import AddStorage from '../../../Storage/AddStorage/AddStorage';
9
9
 
10
- type storageItem = { name: string; id: string; type: string };
10
+ type storageItem = { name: string; id: string; type: string; defaultPath?: string };
11
11
 
12
12
  interface StoragePickerProps {
13
13
  storagePath?: string;
@@ -23,11 +23,16 @@ const StoragePicker = ({ onUpdate, storagePath = '', storageId, disabled = false
23
23
  const [showAddStorageModal, setShowAddStorageModal] = useState(false);
24
24
  const [path, setPath] = useState(() => storagePath);
25
25
  const isLocalStorage = selectedStorage?.type === 'local';
26
+ const hasBucketName = selectedStorage?.defaultPath && selectedStorage?.defaultPath !== '/';
27
+ const fullPath = hasBucketName ? `${selectedStorage.defaultPath}${path ? `/${path}` : ''}` : path;
26
28
 
27
29
  const { data: allStorageData } = useGetStorages();
28
30
  const allUserStorages = (allStorageData?.result as storageItem[]) || [];
29
31
  const allStorages = [...allUserStorages];
30
32
 
33
+ console.log('allStorages :', allStorages);
34
+ console.log('selectedStorage :', selectedStorage);
35
+
31
36
  const storageOptions = useMemo(() => {
32
37
  const storageOpts = allStorages.map(({ name, id, type }) => ({
33
38
  label: name,
@@ -52,12 +57,16 @@ const StoragePicker = ({ onUpdate, storagePath = '', storageId, disabled = false
52
57
  if (allStorages.length > 0 && storageId) {
53
58
  const currentStorage = allStorages.find((s) => s.id === storageId);
54
59
  setSelectedStorage(currentStorage);
60
+ if (currentStorage?.defaultPath && currentStorage.defaultPath !== '/') {
61
+ const prefix = currentStorage.defaultPath + '/';
62
+ setPath((prev) => (prev.startsWith(prefix) ? prev.slice(prefix.length) : prev));
63
+ }
55
64
  }
56
65
  }, [allStorageData]);
57
66
 
58
67
  useEffect(() => {
59
68
  if (selectedStorage) {
60
- onUpdate({ storage: selectedStorage, path });
69
+ onUpdate({ storage: selectedStorage, path: fullPath });
61
70
  }
62
71
  }, [selectedStorage, path]);
63
72
 
@@ -80,12 +89,17 @@ const StoragePicker = ({ onUpdate, storagePath = '', storageId, disabled = false
80
89
  disabled={disabled}
81
90
  />
82
91
  </div>
83
- <div className={classes.path}>
92
+ <div className={`${classes.path} ${hasBucketName ? classes.withBucket : ''}`}>
93
+ {hasBucketName && (
94
+ <span className={classes.defaultPath} title={`Bucket: ${selectedStorage.defaultPath}`}>
95
+ {selectedStorage.defaultPath + '/'}
96
+ </span>
97
+ )}
84
98
  <Input
85
99
  disabled={disabled}
86
100
  fieldValue={path}
87
- onUpdate={(val) => setPath(val)}
88
- placeholder={isLocalStorage ? 'Select a folder' : `folder-or-bucket/subfolder`}
101
+ onUpdate={(val) => setPath(val.startsWith('/') ? val.slice(1) : val)} //if the val starts with a slash remove it
102
+ placeholder={isLocalStorage ? 'Select a folder' : hasBucketName ? 'subfolder' : `folder-or-bucket/subfolder`}
89
103
  full={true}
90
104
  required={!disabled && isLocalStorage}
91
105
  error={(!disabled && isLocalStorage && !path ? 'Required' : '') as string}
@@ -198,3 +198,52 @@ export function useVerifyStorage() {
198
198
  },
199
199
  });
200
200
  }
201
+
202
+ // ── OAuth Authorization ─────────────────────────────────────────────
203
+ export async function startStorageAuthorize(type: string): Promise<{ sessionId: string }> {
204
+ const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
205
+ const res = await fetch(`${API_URL}/storages/authorize`, {
206
+ method: 'POST',
207
+ credentials: 'include',
208
+ headers: header,
209
+ body: JSON.stringify({ type }),
210
+ });
211
+ const data = await res.json();
212
+ if (!data.success) {
213
+ throw new Error(data.error);
214
+ }
215
+ return data.result;
216
+ }
217
+
218
+ export async function getStorageAuthorizeStatus(sessionId: string): Promise<{
219
+ status: 'pending' | 'success' | 'error';
220
+ token?: string;
221
+ error?: string;
222
+ authUrl?: string;
223
+ }> {
224
+ const url = new URL(`${API_URL}/storages/authorize/status`);
225
+ url.searchParams.set('sessionId', sessionId);
226
+ const res = await fetch(url.toString(), {
227
+ method: 'GET',
228
+ credentials: 'include',
229
+ });
230
+ const data = await res.json();
231
+ if (!data.success) {
232
+ throw new Error(data.error);
233
+ }
234
+ return data.result;
235
+ }
236
+
237
+ export async function cancelStorageAuthorize(sessionId: string): Promise<void> {
238
+ const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
239
+ const res = await fetch(`${API_URL}/storages/authorize/cancel`, {
240
+ method: 'POST',
241
+ credentials: 'include',
242
+ headers: header,
243
+ body: JSON.stringify({ sessionId }),
244
+ });
245
+ const data = await res.json();
246
+ if (!data.success) {
247
+ throw new Error(data.error);
248
+ }
249
+ }