Merge pull request #122 from agnosticeng/feat/expand-dataset
This commit is contained in:
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -19,7 +19,7 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "agx"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"serde_json",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import type { Table } from '$lib/olap-engine';
|
||||
|
||||
import SearchBar from '$lib/components/SearchBar.svelte';
|
||||
import CollapseAll from '$lib/icons/CollapseAll.svelte';
|
||||
import { collapseAll, expandAll } from './emitter';
|
||||
import Tree from './Tree.svelte';
|
||||
import { buildTree, filter } from './utils';
|
||||
|
||||
@@ -14,19 +16,77 @@
|
||||
let search = $state<string>('');
|
||||
const filtered = $derived(filter(tables, search));
|
||||
const tree = $derived(buildTree(filtered));
|
||||
|
||||
$effect(() => {
|
||||
if (search) expandAll();
|
||||
else collapseAll();
|
||||
});
|
||||
</script>
|
||||
|
||||
<SearchBar bind:value={search} />
|
||||
|
||||
<div>
|
||||
<div class="datasets">
|
||||
<div class="toolbar">
|
||||
<button onclick={() => collapseAll()}><CollapseAll size="12" /></button>
|
||||
</div>
|
||||
{#each tree as node}
|
||||
<Tree {node} expanded={!!search} />
|
||||
<Tree {node} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
.datasets {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.datasets::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.datasets {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
background-color: hsl(0deg 0% 5%);
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
color: hsl(0deg 0% 83%);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: hsl(0deg 0% 10%);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
|
||||
&:is(:hover, :focus-within):not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,35 +2,45 @@
|
||||
import Folder from '$lib/icons/Folder.svelte';
|
||||
import FolderOpen from '$lib/icons/FolderOpen.svelte';
|
||||
import Table from '$lib/icons/Table.svelte';
|
||||
import { tick } from 'svelte';
|
||||
import Columns from './Columns.svelte';
|
||||
import { onExpand } from './emitter';
|
||||
import { onCollapseAll, onExpand, onExpandAll } from './emitter';
|
||||
import Tree from './Tree.svelte';
|
||||
import { findNodeInTree, type TreeNode } from './utils';
|
||||
|
||||
interface Props {
|
||||
node: TreeNode;
|
||||
level?: number;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
let { node, level = 0, expanded: forceExpanded }: Props = $props();
|
||||
let { node, level = 0 }: Props = $props();
|
||||
|
||||
let expanded = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (typeof forceExpanded === 'boolean') expanded = forceExpanded;
|
||||
});
|
||||
$effect(() => onCollapseAll(() => (expanded = false)));
|
||||
$effect(() => onExpandAll(() => (expanded = true)));
|
||||
|
||||
function toggleExpanded() {
|
||||
expanded = !expanded;
|
||||
}
|
||||
|
||||
$effect(() =>
|
||||
onExpand((value) => {
|
||||
if (node.type === 'dataset' && node.value === value) expanded = true;
|
||||
else if (node.type === 'group' && findNodeInTree(node.children, value)) expanded = true;
|
||||
onExpand(async (value) => {
|
||||
if (node.type === 'dataset' && node.value === value) {
|
||||
expanded = true;
|
||||
await tick();
|
||||
animateExpand(value);
|
||||
} else if (node.type === 'group' && findNodeInTree(node.children, value)) expanded = true;
|
||||
})
|
||||
);
|
||||
|
||||
function animateExpand(id: string) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
element.style.backgroundColor = 'hsl(60 40% 34% / 1)';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="node" style:opacity={expanded ? 1 : 0.7 + level}>
|
||||
@@ -53,7 +63,11 @@
|
||||
{/if}
|
||||
|
||||
{#if node.type == 'dataset'}
|
||||
<span class="name">
|
||||
<span
|
||||
class="name"
|
||||
id={node.value}
|
||||
ontransitionend={(e) => (e.currentTarget.style.backgroundColor = '')}
|
||||
>
|
||||
<Table size={10} />
|
||||
<span>{node.name}</span>
|
||||
</span>
|
||||
@@ -62,7 +76,7 @@
|
||||
{#if node.type === 'group'}
|
||||
<div style:display={expanded ? 'contents' : 'none'}>
|
||||
{#each node.children as child}
|
||||
<Tree node={child} level={level + 1} expanded={forceExpanded} />
|
||||
<Tree node={child} level={level + 1} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -94,6 +108,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
|
||||
transition: background-color linear 0.25s;
|
||||
scroll-margin-top: 30px;
|
||||
}
|
||||
|
||||
.name span {
|
||||
|
||||
@@ -3,6 +3,8 @@ import mitt from 'mitt';
|
||||
|
||||
type Events = {
|
||||
expand: Table['name'];
|
||||
'expand-all': void;
|
||||
'collapse-all': void;
|
||||
};
|
||||
|
||||
const emitter = mitt<Events>();
|
||||
@@ -16,3 +18,23 @@ export function onExpand(handler: (tableName: string) => void) {
|
||||
|
||||
return () => emitter.off('expand', handler);
|
||||
}
|
||||
|
||||
export function expandAll() {
|
||||
emitter.emit('expand-all');
|
||||
}
|
||||
|
||||
export function onExpandAll(handler: () => void) {
|
||||
emitter.on('expand-all', handler);
|
||||
|
||||
return () => emitter.off('expand-all', handler);
|
||||
}
|
||||
|
||||
export function collapseAll() {
|
||||
emitter.emit('collapse-all');
|
||||
}
|
||||
|
||||
export function onCollapseAll(handler: () => void) {
|
||||
emitter.on('collapse-all', handler);
|
||||
|
||||
return () => emitter.off('collapse-all', handler);
|
||||
}
|
||||
|
||||
18
src/lib/icons/CollapseAll.svelte
Normal file
18
src/lib/icons/CollapseAll.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
interface Props extends Omit<SvelteHTMLElements['svg'], 'width' | 'height' | 'fill'> {
|
||||
size?: string | number | null;
|
||||
}
|
||||
|
||||
let { size = 24, color = 'currentColor', ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg width={size} height={size} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill={color}>
|
||||
<path d="M9 9H4v1h5V9z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5 3l1-1h7l1 1v7l-1 1h-2v2l-1 1H3l-1-1V6l1-1h2V3zm1 2h4l1 1v4h2V3H6v2zm4 1H3v7h7V6z"
|
||||
/>
|
||||
</svg>
|
||||
Reference in New Issue
Block a user