@walkthru-earth/objex 1.1.0 → 1.2.0
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/README.md +3 -1
- package/dist/components/browser/FileBrowser.svelte +25 -14
- package/dist/components/browser/FileTreeSidebar.svelte +42 -6
- package/dist/components/layout/ConnectionDialog.svelte +100 -1
- package/dist/components/layout/Sidebar.svelte +43 -25
- package/dist/components/viewers/CodeViewer.svelte +23 -0
- package/dist/components/viewers/CogControls.svelte +208 -0
- package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
- package/dist/components/viewers/CogViewer.svelte +353 -1160
- package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/DatabaseViewer.svelte +345 -37
- package/dist/components/viewers/MarkdownViewer.svelte +1 -1
- package/dist/components/viewers/TableViewer.svelte +123 -41
- package/dist/components/viewers/ZarrMapViewer.svelte +29 -0
- package/dist/components/viewers/ZarrViewer.svelte +1 -4
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +6 -2
- package/dist/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +12 -2
- package/dist/i18n/ar.js +24 -0
- package/dist/i18n/en.js +24 -0
- package/dist/i18n/index.svelte.d.ts +0 -1
- package/dist/i18n/index.svelte.js +0 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/query/engine.d.ts +20 -4
- package/dist/query/index.d.ts +2 -1
- package/dist/query/index.js +1 -0
- package/dist/query/source.d.ts +30 -0
- package/dist/query/source.js +37 -0
- package/dist/query/wasm.d.ts +7 -5
- package/dist/query/wasm.js +138 -85
- package/dist/storage/providers.d.ts +47 -0
- package/dist/storage/providers.js +160 -0
- package/dist/stores/files.svelte.d.ts +1 -2
- package/dist/stores/files.svelte.js +1 -2
- package/dist/stores/tabs.svelte.d.ts +9 -2
- package/dist/stores/tabs.svelte.js +11 -2
- package/dist/types.d.ts +11 -0
- package/dist/utils/cog.d.ts +244 -0
- package/dist/utils/cog.js +1039 -0
- package/dist/utils/deck.d.ts +0 -18
- package/dist/utils/deck.js +0 -36
- package/dist/utils/geometry-type.d.ts +52 -0
- package/dist/utils/geometry-type.js +76 -0
- package/dist/utils/markdown-sql.d.ts +1 -1
- package/dist/utils/markdown-sql.js +3 -4
- package/dist/utils/pmtiles-tile.d.ts +0 -2
- package/dist/utils/pmtiles-tile.js +0 -8
- package/dist/utils/url-state.d.ts +6 -0
- package/dist/utils/url-state.js +34 -26
- package/dist/utils/url.d.ts +13 -9
- package/dist/utils/url.js +16 -25
- package/dist/utils/zarr-tab.d.ts +22 -0
- package/dist/utils/zarr-tab.js +30 -0
- package/dist/utils/zarr.d.ts +0 -2
- package/dist/utils/zarr.js +73 -44
- package/package.json +47 -43
- package/dist/components/ui/tabs/index.d.ts +0 -5
- package/dist/components/ui/tabs/index.js +0 -7
- package/dist/components/ui/tabs/tabs-content.svelte +0 -17
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-list.svelte +0 -16
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs.svelte +0 -19
- package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
- package/dist/components/viewers/MapViewer.svelte +0 -234
- package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
- package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
- package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ import { readParquetMetadata } from '@walkthru-earth/objex/utils/parquet-metadat
|
|
|
63
63
|
import { getFileTypeInfo } from '@walkthru-earth/objex/file-icons';
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Requires `svelte ^5` and `@sveltejs/kit ^2` as peer dependencies. Heavy deps (DuckDB, deck.gl, MapLibre, Arrow, hyparquet) are optional peers -- only install what you need.
|
|
66
|
+
Requires `svelte ^5` and `@sveltejs/kit ^2` as peer dependencies. Heavy deps (DuckDB, deck.gl, MapLibre, Arrow, hyparquet, hyparquet-compressors, yaml) are optional peers -- only install what you need.
|
|
67
67
|
|
|
68
68
|
### `@walkthru-earth/objex-utils` -- Pure TypeScript Utilities
|
|
69
69
|
|
|
@@ -86,6 +86,8 @@ import {
|
|
|
86
86
|
} from '@walkthru-earth/objex-utils';
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
Full per-module reference docs: [`packages/objex-utils/docs/`](packages/objex-utils/docs/README.md).
|
|
90
|
+
|
|
89
91
|
### Exports
|
|
90
92
|
|
|
91
93
|
| Export path | What |
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
toggleSortField
|
|
16
16
|
} from '../../utils/file-sort.js';
|
|
17
17
|
import { detectZarrMarkers } from '../../utils/zarr.js';
|
|
18
|
+
import { openZarrTab } from '../../utils/zarr-tab.js';
|
|
18
19
|
import Breadcrumb from './Breadcrumb.svelte';
|
|
19
20
|
import CreateFolderDialog from './CreateFolderDialog.svelte';
|
|
20
21
|
import DeleteConfirmDialog from './DeleteConfirmDialog.svelte';
|
|
@@ -37,19 +38,22 @@ let showWriteActions = $derived(browser.canWrite && !safeLock.locked);
|
|
|
37
38
|
|
|
38
39
|
const zarrDetection = $derived(detectZarrMarkers(browser.entries.map((e: FileEntry) => e.name)));
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
41
|
+
// Auto-open Zarr viewer when markers are detected in the current directory.
|
|
42
|
+
// Uses a Set to prevent re-triggering when navigating back to a previously opened store.
|
|
43
|
+
const autoOpenedPrefixes = new Set<string>();
|
|
44
|
+
$effect(() => {
|
|
45
|
+
if (zarrDetection.detected && browser.activeConnection) {
|
|
46
|
+
const prefix = browser.currentPrefix;
|
|
47
|
+
if (!autoOpenedPrefixes.has(prefix)) {
|
|
48
|
+
autoOpenedPrefixes.add(prefix);
|
|
49
|
+
openZarrTab(prefix, {
|
|
50
|
+
source: 'remote',
|
|
51
|
+
connectionId: browser.activeConnection.id,
|
|
52
|
+
bucketFallback: browser.activeConnection.bucket
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
53
57
|
|
|
54
58
|
const sortedAndFilteredEntries = $derived.by(() => {
|
|
55
59
|
let result = browser.entries;
|
|
@@ -177,7 +181,14 @@ function handleRename(entry: FileEntry) {
|
|
|
177
181
|
variant="outline"
|
|
178
182
|
size="sm"
|
|
179
183
|
class="h-6 gap-1 px-2 text-[11px]"
|
|
180
|
-
onclick={
|
|
184
|
+
onclick={() => {
|
|
185
|
+
if (!browser.activeConnection) return;
|
|
186
|
+
openZarrTab(browser.currentPrefix, {
|
|
187
|
+
source: 'remote',
|
|
188
|
+
connectionId: browser.activeConnection.id,
|
|
189
|
+
bucketFallback: browser.activeConnection.bucket
|
|
190
|
+
});
|
|
191
|
+
}}
|
|
181
192
|
>
|
|
182
193
|
{t('fileBrowser.openAsZarr')}
|
|
183
194
|
</Button>
|
|
@@ -19,6 +19,8 @@ import { tabs } from '../../stores/tabs.svelte.js';
|
|
|
19
19
|
import type { Connection, FileEntry } from '../../types.js';
|
|
20
20
|
import { getNativeScheme } from '../../utils/cloud-url.js';
|
|
21
21
|
import { syncUrlParam } from '../../utils/url-state.js';
|
|
22
|
+
import { detectZarrMarkers } from '../../utils/zarr.js';
|
|
23
|
+
import { openZarrTab } from '../../utils/zarr-tab.js';
|
|
22
24
|
|
|
23
25
|
let {
|
|
24
26
|
connection,
|
|
@@ -48,6 +50,8 @@ let rootContinuationToken = $state<string | undefined>();
|
|
|
48
50
|
let rootHasMore = $state(false);
|
|
49
51
|
let filterQuery = $state('');
|
|
50
52
|
let scrollEl = $state<HTMLElement>();
|
|
53
|
+
/** Paths of directories detected as Zarr stores (by marker files in children). */
|
|
54
|
+
let detectedZarrPaths = $state(new Set<string>());
|
|
51
55
|
|
|
52
56
|
const filteredNodes = $derived(
|
|
53
57
|
filterQuery ? filterTree(rootNodes, filterQuery.toLowerCase()) : rootNodes
|
|
@@ -156,6 +160,16 @@ async function toggleFolder(node: TreeNode) {
|
|
|
156
160
|
node.expanded = !node.expanded;
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
function openAsZarr(dirPath: string) {
|
|
164
|
+
const path = dirPath.endsWith('/') ? dirPath : `${dirPath}/`;
|
|
165
|
+
openZarrTab(path, {
|
|
166
|
+
source: 'remote',
|
|
167
|
+
connectionId: connection.id,
|
|
168
|
+
bucketFallback: connection.bucket
|
|
169
|
+
});
|
|
170
|
+
syncUrlParam(connection, path);
|
|
171
|
+
}
|
|
172
|
+
|
|
159
173
|
function openFile(entry: FileEntry) {
|
|
160
174
|
tabs.open({
|
|
161
175
|
id: `${connection.id}:${entry.path}`,
|
|
@@ -175,12 +189,27 @@ function isViewerDir(entry: FileEntry): boolean {
|
|
|
175
189
|
return entry.is_dir && VIEWER_DIR_EXTENSIONS.has(entry.extension);
|
|
176
190
|
}
|
|
177
191
|
|
|
178
|
-
|
|
192
|
+
/** Whether a directory should render with the Zarr icon (either by extension or marker detection). */
|
|
193
|
+
function isZarrDir(entry: FileEntry): boolean {
|
|
194
|
+
return isViewerDir(entry) || detectedZarrPaths.has(entry.path);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function handleNodeClick(node: TreeNode) {
|
|
179
198
|
if (isViewerDir(node.entry)) {
|
|
180
|
-
// .zarr directories open in the viewer (clicking chevron expands)
|
|
181
|
-
|
|
199
|
+
// .zarr / .zr3 directories open in the viewer (clicking chevron expands)
|
|
200
|
+
openAsZarr(node.entry.path);
|
|
182
201
|
} else if (node.entry.is_dir) {
|
|
183
|
-
|
|
202
|
+
// Load children if needed, then check for zarr markers
|
|
203
|
+
if (node.children.length === 0) {
|
|
204
|
+
await loadChildren(node);
|
|
205
|
+
}
|
|
206
|
+
const zarrCheck = detectZarrMarkers(node.children.map((c) => c.entry.name));
|
|
207
|
+
if (zarrCheck.detected) {
|
|
208
|
+
detectedZarrPaths.add(node.entry.path);
|
|
209
|
+
openAsZarr(node.entry.path);
|
|
210
|
+
}
|
|
211
|
+
// Always expand so user can also browse contents
|
|
212
|
+
node.expanded = !node.expanded;
|
|
184
213
|
} else {
|
|
185
214
|
openFile(node.entry);
|
|
186
215
|
}
|
|
@@ -349,6 +378,7 @@ async function loadRoot() {
|
|
|
349
378
|
rootLoading = true;
|
|
350
379
|
rootContinuationToken = undefined;
|
|
351
380
|
rootHasMore = false;
|
|
381
|
+
detectedZarrPaths = new Set();
|
|
352
382
|
try {
|
|
353
383
|
const adapter = getAdapter('remote', connection.id);
|
|
354
384
|
const prefix = connection.rootPrefix ?? '';
|
|
@@ -488,8 +518,8 @@ async function loadMoreRoot() {
|
|
|
488
518
|
onclick={() => handleNodeClick(node)}
|
|
489
519
|
>
|
|
490
520
|
<FileTypeIcon
|
|
491
|
-
extension={entry.extension}
|
|
492
|
-
isDir={entry.is_dir && !
|
|
521
|
+
extension={isZarrDir(entry) ? 'zarr' : entry.extension}
|
|
522
|
+
isDir={entry.is_dir && !isZarrDir(entry)}
|
|
493
523
|
isOpen={node.expanded}
|
|
494
524
|
class="size-3.5 shrink-0"
|
|
495
525
|
/>
|
|
@@ -510,6 +540,12 @@ async function loadMoreRoot() {
|
|
|
510
540
|
{t('fileTree.open')}
|
|
511
541
|
</ContextMenu.Item>
|
|
512
542
|
<ContextMenu.Separator />
|
|
543
|
+
{:else if detectedZarrPaths.has(entry.path)}
|
|
544
|
+
<ContextMenu.Item onclick={() => openAsZarr(entry.path)}>
|
|
545
|
+
<ExternalLinkIcon class="me-2 size-3.5" />
|
|
546
|
+
{t('fileBrowser.openAsZarr')}
|
|
547
|
+
</ContextMenu.Item>
|
|
548
|
+
<ContextMenu.Separator />
|
|
513
549
|
{/if}
|
|
514
550
|
|
|
515
551
|
<ContextMenu.Item onclick={() => copyToClipboard(buildHttpUrl(entry.path))}>
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import CheckIcon from '@lucide/svelte/icons/check';
|
|
3
|
+
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
3
4
|
import CloudIcon from '@lucide/svelte/icons/cloud';
|
|
5
|
+
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
|
6
|
+
import GlobeIcon from '@lucide/svelte/icons/globe';
|
|
4
7
|
import LinkIcon from '@lucide/svelte/icons/link';
|
|
5
8
|
import Loader2Icon from '@lucide/svelte/icons/loader-2';
|
|
6
9
|
import LockIcon from '@lucide/svelte/icons/lock';
|
|
7
10
|
import PlugZapIcon from '@lucide/svelte/icons/plug-zap';
|
|
11
|
+
import ShieldIcon from '@lucide/svelte/icons/shield';
|
|
8
12
|
import XIcon from '@lucide/svelte/icons/x';
|
|
9
13
|
import { Button } from '../ui/button/index.js';
|
|
10
14
|
import { Input } from '../ui/input/index.js';
|
|
@@ -20,10 +24,12 @@ import { Switch } from '../ui/switch/index.js';
|
|
|
20
24
|
import { t } from '../../i18n/index.svelte.js';
|
|
21
25
|
import {
|
|
22
26
|
buildEndpointFromTemplate,
|
|
27
|
+
CORS_HELP,
|
|
23
28
|
getProvider,
|
|
24
29
|
PROVIDER_IDS,
|
|
25
30
|
PROVIDERS,
|
|
26
|
-
type ProviderId
|
|
31
|
+
type ProviderId,
|
|
32
|
+
READ_ONLY_HELP
|
|
27
33
|
} from '../../storage/providers.js';
|
|
28
34
|
import { connections } from '../../stores/connections.svelte.js';
|
|
29
35
|
import type { Connection, ConnectionConfig } from '../../types.js';
|
|
@@ -71,6 +77,8 @@ let isAzure = $derived(provider === 'azure');
|
|
|
71
77
|
let hasRegions = $derived(providerDef.regions.length > 0);
|
|
72
78
|
let needsRegion = $derived(providerDef.needsRegion);
|
|
73
79
|
let bucketLabel = $derived(providerDef.bucketLabel ?? t('connection.bucket'));
|
|
80
|
+
let corsHelp = $derived(CORS_HELP[provider]);
|
|
81
|
+
let readOnlyHelp = $derived(READ_ONLY_HELP[provider]);
|
|
74
82
|
|
|
75
83
|
let isEditMode = $derived(editConnection !== null && editConnection !== undefined);
|
|
76
84
|
let title = $derived(isEditMode ? t('connection.editTitle') : t('connection.newTitle'));
|
|
@@ -433,6 +441,97 @@ async function handleTestConnection() {
|
|
|
433
441
|
</form>
|
|
434
442
|
{/if}
|
|
435
443
|
|
|
444
|
+
<!-- CORS Help -->
|
|
445
|
+
{#if corsHelp}
|
|
446
|
+
<details class="group rounded-md border border-border">
|
|
447
|
+
<summary class="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground [&::-webkit-details-marker]:hidden">
|
|
448
|
+
<ChevronRightIcon class="size-3.5 shrink-0 transition-transform group-open:rotate-90" />
|
|
449
|
+
<GlobeIcon class="size-3.5 shrink-0" />
|
|
450
|
+
{t('connection.corsTitle')}
|
|
451
|
+
</summary>
|
|
452
|
+
<div class="flex flex-col gap-2.5 border-t border-border px-3 py-2.5">
|
|
453
|
+
{#if corsHelp.defaultEnabled}
|
|
454
|
+
<div class="flex items-center gap-1.5 text-xs text-green-700 dark:text-green-400">
|
|
455
|
+
<CheckIcon class="size-3 shrink-0" />
|
|
456
|
+
<span>{t('connection.corsDefault')}</span>
|
|
457
|
+
</div>
|
|
458
|
+
{:else}
|
|
459
|
+
<p class="text-xs text-muted-foreground">{t('connection.corsRequired')}</p>
|
|
460
|
+
{/if}
|
|
461
|
+
|
|
462
|
+
{#if corsHelp.note}
|
|
463
|
+
<p class="text-xs text-muted-foreground">{corsHelp.note}</p>
|
|
464
|
+
{/if}
|
|
465
|
+
|
|
466
|
+
{#if corsHelp.docsUrl}
|
|
467
|
+
<a
|
|
468
|
+
href={corsHelp.docsUrl}
|
|
469
|
+
target="_blank"
|
|
470
|
+
rel="noopener noreferrer"
|
|
471
|
+
class="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
|
472
|
+
>
|
|
473
|
+
<ExternalLinkIcon class="size-3 shrink-0" />
|
|
474
|
+
{t('connection.corsDocs')}
|
|
475
|
+
</a>
|
|
476
|
+
{/if}
|
|
477
|
+
|
|
478
|
+
{#if corsHelp.cliSteps && corsHelp.cliSteps.length > 0}
|
|
479
|
+
<details class="group/cli">
|
|
480
|
+
<summary class="flex cursor-pointer list-none items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground [&::-webkit-details-marker]:hidden">
|
|
481
|
+
<ChevronRightIcon class="size-3 shrink-0 transition-transform group-open/cli:rotate-90" />
|
|
482
|
+
{t('connection.corsCliTitle')}
|
|
483
|
+
</summary>
|
|
484
|
+
<div class="mt-1.5 flex flex-col gap-1.5">
|
|
485
|
+
{#each corsHelp.cliSteps as step, i}
|
|
486
|
+
<pre class="overflow-x-auto rounded bg-muted px-2.5 py-2 text-[11px] leading-relaxed">{step}</pre>
|
|
487
|
+
{/each}
|
|
488
|
+
</div>
|
|
489
|
+
</details>
|
|
490
|
+
{/if}
|
|
491
|
+
</div>
|
|
492
|
+
</details>
|
|
493
|
+
{/if}
|
|
494
|
+
|
|
495
|
+
<!-- Read-Only Access Help -->
|
|
496
|
+
{#if readOnlyHelp}
|
|
497
|
+
<details class="group rounded-md border border-border">
|
|
498
|
+
<summary class="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground [&::-webkit-details-marker]:hidden">
|
|
499
|
+
<ChevronRightIcon class="size-3.5 shrink-0 transition-transform group-open:rotate-90" />
|
|
500
|
+
<ShieldIcon class="size-3.5 shrink-0" />
|
|
501
|
+
{t('connection.readOnlyTitle')}
|
|
502
|
+
</summary>
|
|
503
|
+
<div class="flex flex-col gap-2.5 border-t border-border px-3 py-2.5">
|
|
504
|
+
<p class="text-xs text-muted-foreground">{readOnlyHelp.note}</p>
|
|
505
|
+
|
|
506
|
+
{#if readOnlyHelp.docsUrl}
|
|
507
|
+
<a
|
|
508
|
+
href={readOnlyHelp.docsUrl}
|
|
509
|
+
target="_blank"
|
|
510
|
+
rel="noopener noreferrer"
|
|
511
|
+
class="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
|
512
|
+
>
|
|
513
|
+
<ExternalLinkIcon class="size-3 shrink-0" />
|
|
514
|
+
{t('connection.readOnlyDocs')}
|
|
515
|
+
</a>
|
|
516
|
+
{/if}
|
|
517
|
+
|
|
518
|
+
{#if readOnlyHelp.cliSteps && readOnlyHelp.cliSteps.length > 0}
|
|
519
|
+
<details class="group/ro">
|
|
520
|
+
<summary class="flex cursor-pointer list-none items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground [&::-webkit-details-marker]:hidden">
|
|
521
|
+
<ChevronRightIcon class="size-3 shrink-0 transition-transform group-open/ro:rotate-90" />
|
|
522
|
+
{t('connection.readOnlyCliTitle')}
|
|
523
|
+
</summary>
|
|
524
|
+
<div class="mt-1.5 flex flex-col gap-1.5">
|
|
525
|
+
{#each readOnlyHelp.cliSteps as step, i}
|
|
526
|
+
<pre class="overflow-x-auto rounded bg-muted px-2.5 py-2 text-[11px] leading-relaxed">{step}</pre>
|
|
527
|
+
{/each}
|
|
528
|
+
</div>
|
|
529
|
+
</details>
|
|
530
|
+
{/if}
|
|
531
|
+
</div>
|
|
532
|
+
</details>
|
|
533
|
+
{/if}
|
|
534
|
+
|
|
436
535
|
<!-- Test Connection Result -->
|
|
437
536
|
{#if testResult === 'success'}
|
|
438
537
|
<div class="flex items-center gap-2 rounded-md border border-green-500/30 bg-green-500/10 px-3 py-2 text-sm text-green-700 dark:text-green-400">
|
|
@@ -23,7 +23,7 @@ import { t } from '../../i18n/index.svelte.js';
|
|
|
23
23
|
import { browser } from '../../stores/browser.svelte.js';
|
|
24
24
|
import { connections } from '../../stores/connections.svelte.js';
|
|
25
25
|
import { credentialStore, loadFromNative } from '../../stores/credentials.svelte.js';
|
|
26
|
-
import { tabs } from '../../stores/tabs.svelte.js';
|
|
26
|
+
import { eagerUrlTabId, tabs } from '../../stores/tabs.svelte.js';
|
|
27
27
|
import type { Connection } from '../../types.js';
|
|
28
28
|
import { type DetectedHost, detectHostBucket } from '../../utils/host-detection.js';
|
|
29
29
|
import { parseStorageUrl } from '../../utils/storage-url.js';
|
|
@@ -58,16 +58,25 @@ $effect(() => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
async function handleAutoDetection() {
|
|
61
|
-
// Direct file URLs (e.g. ?url=https://...file.parquet) are opened eagerly
|
|
62
|
-
// in +page.svelte so they work on mobile. Skip if tab already exists.
|
|
63
61
|
const url = new URL(window.location.href);
|
|
64
62
|
const rawUrl = url.searchParams.get('url');
|
|
65
|
-
|
|
63
|
+
|
|
64
|
+
const detected = detectHostBucket();
|
|
65
|
+
if (!detected) {
|
|
66
|
+
// No recognizable host — let the eager URL tab handle it
|
|
66
67
|
return;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
if
|
|
70
|
+
// A recognizable storage provider was detected. Close the eagerly-opened
|
|
71
|
+
// URL tab (if any) so we can re-open it with a proper connectionId that
|
|
72
|
+
// provides S3 credentials and endpoint config for DuckDB httpfs.
|
|
73
|
+
if (rawUrl) {
|
|
74
|
+
const eagerTabId = eagerUrlTabId(rawUrl);
|
|
75
|
+
const eagerTab = tabs.items.find((t) => t.id === eagerTabId);
|
|
76
|
+
if (eagerTab) {
|
|
77
|
+
tabs.close(eagerTabId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
71
80
|
|
|
72
81
|
const hasUrlParam = url.searchParams.has('url');
|
|
73
82
|
|
|
@@ -85,27 +94,36 @@ async function handleAutoDetection() {
|
|
|
85
94
|
const prefixParam = parsed.prefix;
|
|
86
95
|
|
|
87
96
|
if (prefixParam && !prefixParam.endsWith('/')) {
|
|
88
|
-
// It's a file — browse to its parent folder and open it
|
|
89
|
-
const parentPrefix = prefixParam.includes('/') ? prefixParam.replace(/\/[^/]*$/, '/') : '';
|
|
90
|
-
browser.browse(conn, parentPrefix || undefined);
|
|
91
97
|
const fileName = prefixParam.split('/').pop() || prefixParam;
|
|
92
98
|
const ext = fileName.includes('.') ? fileName.split('.').pop()!.toLowerCase() : '';
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
})
|
|
108
|
-
|
|
99
|
+
if (ext) {
|
|
100
|
+
// It's a file — browse to its parent folder and open it
|
|
101
|
+
const parentPrefix = prefixParam.includes('/')
|
|
102
|
+
? prefixParam.replace(/\/[^/]*$/, '/')
|
|
103
|
+
: '';
|
|
104
|
+
browser.browse(conn, parentPrefix || undefined);
|
|
105
|
+
const tabId = `${conn.id}:${prefixParam}`;
|
|
106
|
+
tabs.open({
|
|
107
|
+
id: tabId,
|
|
108
|
+
name: fileName,
|
|
109
|
+
path: prefixParam,
|
|
110
|
+
source: 'remote',
|
|
111
|
+
connectionId: conn.id,
|
|
112
|
+
extension: ext
|
|
113
|
+
});
|
|
114
|
+
// Fire-and-forget: fetch file size via HEAD request
|
|
115
|
+
fetch(url.searchParams.get('url')!, { method: 'HEAD' })
|
|
116
|
+
.then((res) => {
|
|
117
|
+
const cl = res.headers.get('content-length');
|
|
118
|
+
if (cl) tabs.update(tabId, { size: Number(cl) });
|
|
119
|
+
})
|
|
120
|
+
.catch(() => {});
|
|
121
|
+
} else {
|
|
122
|
+
// No extension — likely a directory (e.g. Zarr store without .zarr suffix).
|
|
123
|
+
// Browse into it and let FileBrowser's auto-detection handle Zarr/etc.
|
|
124
|
+
const dirPrefix = `${prefixParam}/`;
|
|
125
|
+
browser.browse(conn, dirPrefix);
|
|
126
|
+
}
|
|
109
127
|
} else if (prefixParam) {
|
|
110
128
|
// It's a directory prefix
|
|
111
129
|
browser.browse(conn, prefixParam);
|
|
@@ -13,6 +13,7 @@ import { handleLoadError } from '../../utils/error.js';
|
|
|
13
13
|
import { extensionToShikiLang, highlightCode } from '../../utils/shiki';
|
|
14
14
|
import { buildHttpsUrl } from '../../utils/url.js';
|
|
15
15
|
import { getUrlView, updateUrlView } from '../../utils/url-state.js';
|
|
16
|
+
import { openZarrTab } from '../../utils/zarr-tab.js';
|
|
16
17
|
|
|
17
18
|
let { tab }: { tab: Tab } = $props();
|
|
18
19
|
|
|
@@ -49,6 +50,8 @@ type JsonKind =
|
|
|
49
50
|
| 'stac-collection'
|
|
50
51
|
| 'stac-item'
|
|
51
52
|
| 'kepler'
|
|
53
|
+
| 'zarr-v2'
|
|
54
|
+
| 'zarr-v3'
|
|
52
55
|
| null;
|
|
53
56
|
|
|
54
57
|
/** Detect if a .py file is a marimo notebook (first 512 bytes contain both markers) */
|
|
@@ -74,6 +77,8 @@ function detectJsonKind(code: string): JsonKind {
|
|
|
74
77
|
if (obj.type === 'Collection' && obj.stac_version) return 'stac-collection';
|
|
75
78
|
if (obj.type === 'Feature' && obj.stac_version) return 'stac-item';
|
|
76
79
|
if (obj.info?.app === 'kepler.gl' && obj.config) return 'kepler';
|
|
80
|
+
if (obj.zarr_format === 3) return 'zarr-v3';
|
|
81
|
+
if (obj.zarr_format === 2) return 'zarr-v2';
|
|
77
82
|
}
|
|
78
83
|
} catch {
|
|
79
84
|
// not valid JSON
|
|
@@ -353,6 +358,24 @@ async function copyCode() {
|
|
|
353
358
|
>
|
|
354
359
|
{viewMode === 'kepler' ? t('code.code') : t('code.openKepler')}
|
|
355
360
|
</Button>
|
|
361
|
+
{:else if jsonKind === 'zarr-v3' || jsonKind === 'zarr-v2'}
|
|
362
|
+
<Badge variant="outline" class="hidden border-purple-200 text-purple-600 sm:inline-flex dark:border-purple-800 dark:text-purple-300">
|
|
363
|
+
{jsonKind === 'zarr-v3' ? 'Zarr v3' : 'Zarr v2'}
|
|
364
|
+
</Badge>
|
|
365
|
+
<Button
|
|
366
|
+
variant="outline"
|
|
367
|
+
size="sm"
|
|
368
|
+
class="h-7 gap-1 px-2 text-xs border-purple-300 text-purple-600 hover:bg-purple-50 hover:text-purple-700 dark:border-purple-700 dark:text-purple-400 dark:hover:bg-purple-950"
|
|
369
|
+
onclick={() => {
|
|
370
|
+
const parentPath = tab.path.replace(/[^/]+$/, '');
|
|
371
|
+
openZarrTab(parentPath, {
|
|
372
|
+
source: tab.source as 'remote' | 'url',
|
|
373
|
+
connectionId: tab.connectionId
|
|
374
|
+
});
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
{t('fileBrowser.openAsZarr')}
|
|
378
|
+
</Button>
|
|
356
379
|
{/if}
|
|
357
380
|
|
|
358
381
|
{#if isMarimo}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '../../i18n/index.svelte.js';
|
|
3
|
+
import {
|
|
4
|
+
type BandConfig,
|
|
5
|
+
COLOR_RAMP_STOPS,
|
|
6
|
+
type ColorRampId,
|
|
7
|
+
DEFAULT_RESCALE,
|
|
8
|
+
type RescaleConfig,
|
|
9
|
+
rampToGradientCss
|
|
10
|
+
} from '../../utils/cog.js';
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
bandCount,
|
|
14
|
+
bandConfig,
|
|
15
|
+
onConfigChange,
|
|
16
|
+
rescale,
|
|
17
|
+
rescaleApplicable,
|
|
18
|
+
onRescaleChange
|
|
19
|
+
}: {
|
|
20
|
+
bandCount: number;
|
|
21
|
+
bandConfig: BandConfig;
|
|
22
|
+
onConfigChange: (config: BandConfig) => void;
|
|
23
|
+
rescale: RescaleConfig;
|
|
24
|
+
rescaleApplicable: boolean;
|
|
25
|
+
onRescaleChange: (rescale: RescaleConfig) => void;
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
const RAMP_IDS: ColorRampId[] = ['grayscale', 'terrain', 'viridis', 'magma', 'turbo', 'spectral'];
|
|
29
|
+
|
|
30
|
+
function bandOptions(count: number): { value: number; label: string }[] {
|
|
31
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
32
|
+
value: i,
|
|
33
|
+
label: `${t('cog.band')} ${i + 1}`
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setMode(mode: 'rgb' | 'single') {
|
|
38
|
+
onConfigChange({ ...bandConfig, mode });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setBand(key: 'rBand' | 'gBand' | 'bBand' | 'band', value: number) {
|
|
42
|
+
onConfigChange({ ...bandConfig, [key]: value });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setRamp(id: ColorRampId) {
|
|
46
|
+
onConfigChange({ ...bandConfig, colorRamp: id });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setRescaleMin(value: number) {
|
|
50
|
+
// Keep min strictly less than max, clamp to [0, 1].
|
|
51
|
+
const clamped = Math.max(0, Math.min(1, value));
|
|
52
|
+
const next = Math.min(clamped, rescale.max - 0.001);
|
|
53
|
+
onRescaleChange({ min: Number.isFinite(next) ? next : 0, max: rescale.max });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setRescaleMax(value: number) {
|
|
57
|
+
const clamped = Math.max(0, Math.min(1, value));
|
|
58
|
+
const next = Math.max(clamped, rescale.min + 0.001);
|
|
59
|
+
onRescaleChange({ min: rescale.min, max: Number.isFinite(next) ? next : 1 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resetRescale() {
|
|
63
|
+
onRescaleChange({ ...DEFAULT_RESCALE });
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<div
|
|
68
|
+
class="absolute right-2 top-10 z-10 w-52 rounded bg-card/90 p-2.5 text-xs text-card-foreground backdrop-blur-sm"
|
|
69
|
+
>
|
|
70
|
+
<!-- Mode toggle -->
|
|
71
|
+
<div class="mb-2 flex gap-1">
|
|
72
|
+
<button
|
|
73
|
+
class="flex-1 rounded px-2 py-1 transition-colors"
|
|
74
|
+
class:bg-primary={bandConfig.mode === 'rgb'}
|
|
75
|
+
class:text-primary-foreground={bandConfig.mode === 'rgb'}
|
|
76
|
+
class:bg-muted={bandConfig.mode !== 'rgb'}
|
|
77
|
+
onclick={() => setMode('rgb')}
|
|
78
|
+
>
|
|
79
|
+
RGB
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
class="flex-1 rounded px-2 py-1 transition-colors"
|
|
83
|
+
class:bg-primary={bandConfig.mode === 'single'}
|
|
84
|
+
class:text-primary-foreground={bandConfig.mode === 'single'}
|
|
85
|
+
class:bg-muted={bandConfig.mode !== 'single'}
|
|
86
|
+
onclick={() => setMode('single')}
|
|
87
|
+
>
|
|
88
|
+
{t('cog.singleBand')}
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{#if bandConfig.mode === 'rgb'}
|
|
93
|
+
<!-- RGB band selectors -->
|
|
94
|
+
<div class="space-y-1">
|
|
95
|
+
{#each [
|
|
96
|
+
{ key: 'rBand' as const, label: 'R', color: 'text-red-400' },
|
|
97
|
+
{ key: 'gBand' as const, label: 'G', color: 'text-green-400' },
|
|
98
|
+
{ key: 'bBand' as const, label: 'B', color: 'text-blue-400' }
|
|
99
|
+
] as ch}
|
|
100
|
+
<div class="flex items-center gap-2">
|
|
101
|
+
<span class="w-3 font-bold {ch.color}">{ch.label}</span>
|
|
102
|
+
<select
|
|
103
|
+
class="flex-1 rounded border border-border bg-background px-1.5 py-0.5 text-xs"
|
|
104
|
+
value={bandConfig[ch.key]}
|
|
105
|
+
onchange={(e) =>
|
|
106
|
+
setBand(ch.key, Number((e.target as HTMLSelectElement).value))}
|
|
107
|
+
>
|
|
108
|
+
{#each bandOptions(bandCount) as opt}
|
|
109
|
+
<option value={opt.value}>{opt.label}</option>
|
|
110
|
+
{/each}
|
|
111
|
+
</select>
|
|
112
|
+
</div>
|
|
113
|
+
{/each}
|
|
114
|
+
</div>
|
|
115
|
+
{:else}
|
|
116
|
+
<!-- Single band selector -->
|
|
117
|
+
<div class="mb-2 flex items-center gap-2">
|
|
118
|
+
<span class="text-muted-foreground">{t('cog.band')}</span>
|
|
119
|
+
<select
|
|
120
|
+
class="flex-1 rounded border border-border bg-background px-1.5 py-0.5 text-xs"
|
|
121
|
+
value={bandConfig.band}
|
|
122
|
+
onchange={(e) =>
|
|
123
|
+
setBand('band', Number((e.target as HTMLSelectElement).value))}
|
|
124
|
+
>
|
|
125
|
+
{#each bandOptions(bandCount) as opt}
|
|
126
|
+
<option value={opt.value}>{opt.label}</option>
|
|
127
|
+
{/each}
|
|
128
|
+
</select>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<!-- Color ramp picker -->
|
|
132
|
+
<div class="space-y-1">
|
|
133
|
+
<span class="text-muted-foreground">{t('cog.colorRamp')}</span>
|
|
134
|
+
<div class="grid grid-cols-2 gap-1">
|
|
135
|
+
{#each RAMP_IDS as id}
|
|
136
|
+
<button
|
|
137
|
+
class="flex flex-col items-stretch rounded border px-1 py-0.5 transition-colors {bandConfig.colorRamp === id ? 'border-primary bg-muted' : 'border-transparent'}"
|
|
138
|
+
onclick={() => setRamp(id)}
|
|
139
|
+
title={id}
|
|
140
|
+
>
|
|
141
|
+
<div
|
|
142
|
+
class="h-2.5 w-full rounded-sm"
|
|
143
|
+
style="background: {rampToGradientCss(id)}"
|
|
144
|
+
></div>
|
|
145
|
+
<span class="mt-0.5 text-center text-[10px] capitalize text-muted-foreground">
|
|
146
|
+
{id}
|
|
147
|
+
</span>
|
|
148
|
+
</button>
|
|
149
|
+
{/each}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
{/if}
|
|
153
|
+
|
|
154
|
+
{#if rescaleApplicable}
|
|
155
|
+
<!-- GPU LinearRescale slider. Default uint pipeline only. -->
|
|
156
|
+
<div class="mt-2 space-y-1 border-t border-border pt-2">
|
|
157
|
+
<div class="flex items-center justify-between">
|
|
158
|
+
<span class="text-muted-foreground">{t('cog.rescale')}</span>
|
|
159
|
+
<button
|
|
160
|
+
class="text-[10px] text-muted-foreground hover:text-card-foreground"
|
|
161
|
+
onclick={resetRescale}
|
|
162
|
+
>
|
|
163
|
+
{t('cog.rescaleReset')}
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="flex items-center gap-1.5">
|
|
167
|
+
<input
|
|
168
|
+
type="number"
|
|
169
|
+
min="0"
|
|
170
|
+
max="1"
|
|
171
|
+
step="0.01"
|
|
172
|
+
class="w-14 rounded border border-border bg-background px-1 py-0.5 text-[11px] tabular-nums"
|
|
173
|
+
value={rescale.min}
|
|
174
|
+
oninput={(e) => setRescaleMin(Number((e.target as HTMLInputElement).value))}
|
|
175
|
+
/>
|
|
176
|
+
<input
|
|
177
|
+
type="range"
|
|
178
|
+
min="0"
|
|
179
|
+
max="1"
|
|
180
|
+
step="0.01"
|
|
181
|
+
class="flex-1 accent-primary"
|
|
182
|
+
value={rescale.min}
|
|
183
|
+
oninput={(e) => setRescaleMin(Number((e.target as HTMLInputElement).value))}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="flex items-center gap-1.5">
|
|
187
|
+
<input
|
|
188
|
+
type="number"
|
|
189
|
+
min="0"
|
|
190
|
+
max="1"
|
|
191
|
+
step="0.01"
|
|
192
|
+
class="w-14 rounded border border-border bg-background px-1 py-0.5 text-[11px] tabular-nums"
|
|
193
|
+
value={rescale.max}
|
|
194
|
+
oninput={(e) => setRescaleMax(Number((e.target as HTMLInputElement).value))}
|
|
195
|
+
/>
|
|
196
|
+
<input
|
|
197
|
+
type="range"
|
|
198
|
+
min="0"
|
|
199
|
+
max="1"
|
|
200
|
+
step="0.01"
|
|
201
|
+
class="flex-1 accent-primary"
|
|
202
|
+
value={rescale.max}
|
|
203
|
+
oninput={(e) => setRescaleMax(Number((e.target as HTMLInputElement).value))}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
{/if}
|
|
208
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type BandConfig, type RescaleConfig } from '../../utils/cog.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
bandCount: number;
|
|
4
|
+
bandConfig: BandConfig;
|
|
5
|
+
onConfigChange: (config: BandConfig) => void;
|
|
6
|
+
rescale: RescaleConfig;
|
|
7
|
+
rescaleApplicable: boolean;
|
|
8
|
+
onRescaleChange: (rescale: RescaleConfig) => void;
|
|
9
|
+
};
|
|
10
|
+
declare const CogControls: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type CogControls = ReturnType<typeof CogControls>;
|
|
12
|
+
export default CogControls;
|