Skip to content

Commit

Permalink
Json tree view optimization (#3694)
Browse files Browse the repository at this point in the history
# Motivation

This is the second iteration of json tree improvement.

# Changes

- mobile layout for actions (header and horizontal scroll)
- values with known units

# Tests

- for utils

# Todos

- [ ] Add entry to changelog (if necessary).
Not necessary

# Screenshots

### Section header
| Before | After |
|--------|--------|
| <img width="390" alt="image"
src="https://github.com/dfinity/nns-dapp/assets/98811342/3902c72c-ace1-4e7b-a149-089ece2215ac">
| <img width="388" alt="image"
src="https://github.com/dfinity/nns-dapp/assets/98811342/9d1691fa-3a44-4c7c-a044-c7c269706eb6">
|

### Values with units and e8s (gaps)
<img width="828" alt="image"
src="https://github.com/dfinity/nns-dapp/assets/98811342/4a000d86-70e8-4b22-af12-e36857afb2e5">
  • Loading branch information
mstrasinskis committed Nov 8, 2023
1 parent ca96e75 commit d4a4d20
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 96 deletions.
77 changes: 43 additions & 34 deletions frontend/src/lib/components/common/JsonPreview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import RawJson from "$lib/components/common/RawJson.svelte";
import { fade } from "svelte/transition";
import { jsonRepresentationModeStore } from "$lib/derived/json-representation.derived";
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
const DEFAULT_EXPANDED_LEVEL = 1;
Expand All @@ -27,36 +28,38 @@
};
</script>

<div class="content-cell-island markdown-container">
{#if $jsonRepresentationModeStore === "tree"}
<div class="json" data-tid="json-wrapper" in:fade>
{#if isExpandedAllVisible}
<button class="ghost expand-all" on:click={toggleExpanded}>
{#if isAllExpanded}
<div in:fade>
<IconCollapseAll />
<span class="expand-all-label">{$i18n.core.collapse_all}</span>
</div>
{:else}
<div in:fade>
<IconExpandAll />
<span class="expand-all-label">{$i18n.core.expand_all}</span>
</div>
{/if}
</button>
<div class="content-cell-island markdown-container" data-tid="json-wrapper">
{#if $jsonRepresentationModeStore === "tree" && isExpandedAllVisible}
<button class="ghost expand-all" on:click={toggleExpanded}>
{#if isAllExpanded}
<div in:fade>
<IconCollapseAll />
<span class="expand-all-label">{$i18n.core.collapse_all}</span>
</div>
{:else}
<div in:fade>
<IconExpandAll />
<span class="expand-all-label">{$i18n.core.expand_all}</span>
</div>
{/if}
<TreeJson
json={expandedData}
defaultExpandedLevel={isAllExpanded
? Number.MAX_SAFE_INTEGER
: DEFAULT_EXPANDED_LEVEL}
/>
</div>
{:else}
<div in:fade>
<RawJson {json} />
</div>
</button>
{/if}
<div class="json-container">
<TestIdWrapper testId="json-wrapper">
{#if $jsonRepresentationModeStore === "tree"}
<div in:fade>
<TreeJson
json={expandedData}
defaultExpandedLevel={isAllExpanded ? Number.MAX_SAFE_INTEGER : 1}
/>
</div>
{:else}
<div in:fade>
<RawJson {json} />
</div>
{/if}
</TestIdWrapper>
</div>
</div>

<style lang="scss">
Expand All @@ -65,8 +68,8 @@
.expand-all {
padding: 0;
position: absolute;
right: var(--padding-0_5x);
top: var(--padding-0_5x);
right: var(--padding-2x);
top: var(--padding-2x);
div {
display: flex;
Expand All @@ -83,16 +86,22 @@
}
}
.json {
// needs for the expand all button
position: relative;
word-break: break-word;
.json-container {
// json content scrolling
overflow-x: auto;
// same as "content-cell-island"
padding: var(--padding-2x);
}
// TODO(max): rename and move to gix-components
.markdown-container {
// custom island styles
background: var(--card-background-disabled);
color: var(--description-color);
// reset "content-cell-island" padding
padding: 0;
// to place expand-all button
position: relative;
}
</style>
1 change: 0 additions & 1 deletion frontend/src/lib/components/common/RawJson.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
pre {
margin: 0;
overflow-x: auto;
@include fonts.small;
}
</style>
61 changes: 41 additions & 20 deletions frontend/src/lib/components/common/TreeJson.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
import { i18n } from "$lib/stores/i18n";
import { IconExpandMore } from "@dfinity/gix-components";
import TreeJsonValue from "$lib/components/common/TreeJsonValue.svelte";
import { getTreeJsonValueRenderType } from "$lib/utils/json.utils";
import {
getTreeJsonValueRenderType,
type TreeJsonValueType,
} from "$lib/utils/json.utils";
import { fade } from "svelte/transition";
import { typeOfLikeANumber } from "$lib/utils/utils";
export let json: unknown | undefined = undefined;
export let defaultExpandedLevel = Infinity;
export let _key: string | undefined = undefined;
export let _level = 0;
// because the child doesn't know if its parent type
export let _isArrayEntry: boolean | undefined = undefined;
let keyLabel: string;
let children: [string, unknown][];
Expand All @@ -19,8 +25,10 @@
let root: boolean;
let keyRoot: boolean;
let testId: "json" | undefined;
let valueType: TreeJsonValueType;
$: {
isExpandable = getTreeJsonValueRenderType(json) === "object";
valueType = getTreeJsonValueRenderType(json);
isExpandable = valueType === "object";
keyLabel = `${_key ?? ""}`;
children = isExpandable ? Object.entries(json as object) : [];
hasChildren = children.length > 0;
Expand All @@ -36,7 +44,7 @@
$: collapsed = _level >= defaultExpandedLevel;
let keyIsIndex = false;
$: keyIsIndex = !isNaN(Number(_key));
$: keyIsIndex = typeOfLikeANumber(_key) || (_isArrayEntry ?? false);
const toggle = () => (collapsed = !collapsed);
</script>
Expand All @@ -46,13 +54,13 @@
{#if _level > 0 && keyLabel}
<!-- expandable-key-->
<div
class="key key--expandable"
class="key expandable"
class:root={keyRoot}
class:key--is-index={keyIsIndex}
class:key-is-index={keyIsIndex}
>
<button
class="icon-only expand-button"
class:expand-button--expanded={!collapsed}
class:is-expandable={!collapsed}
data-tid={testId}
aria-label={$i18n.core.toggle}
tabindex="0"
Expand All @@ -67,36 +75,36 @@
<!-- children of expandable-key -->
<ul class:root class:is-array={isArray} in:fade>
{#each children as [key, value]}
<li class:root>
<li class:root class:key-is-index={keyIsIndex}>
<svelte:self
json={value}
_key={key}
{defaultExpandedLevel}
_level={_level + 1}
_isArrayEntry={isArray}
/>
</li>
{/each}
</ul>
{/if}
{:else if isExpandable}
<!-- expandable w/o children - key+{}|[] -->
<span data-tid={testId} class="key-value">
<span data-tid={testId} class="key-value" class:key-is-index={keyIsIndex}>
{#if keyLabel !== ""}<span
class="key key--no-expand-button"
class="key"
class:root={keyRoot}
class:key--is-index={keyIsIndex}>{keyLabel}</span
class:key-is-index={_isArrayEntry}>{keyLabel}</span
>{/if}
<span class="value">{emptyExpandableValue}</span>
</span>
{:else}
<!-- key+value -->
<span class="key-value">
{#if keyLabel !== ""}<span
class="key key--no-expand-button"
class:root={keyRoot}
class:key--is-index={keyIsIndex}>{keyLabel}</span
>{/if}
<TreeJsonValue data={json} key={_key} />
<span class={`key-value ${valueType}`} class:key-is-index={keyIsIndex}>
{#if keyLabel !== ""}
<span class="key" class:key-is-index={keyIsIndex} class:root={keyRoot}>
{keyLabel}
</span>{/if}
<TreeJsonValue data={json} key={_key} {valueType} />
</span>
{/if}

Expand Down Expand Up @@ -124,10 +132,23 @@
display: flex;
align-items: center;
// To improve long lines mobile readability
flex-wrap: wrap;
row-gap: calc(var(--padding-0_5x) / 2);
// To have at least the same height as IconExpandMore
min-height: 28px;
// icon gap compensation
margin-left: 6px;
// don't wrap array entries because keys are short (`0: "..."`)
&.key-is-index {
flex-wrap: nowrap;
}
// don't wrap images because of broken zoom animation
&.base64Encoding {
flex-wrap: nowrap;
}
}
.key {
Expand All @@ -138,12 +159,12 @@
@include fonts.standard(true);
color: var(--content-color);
&.key--expandable {
&.expandable {
margin-right: 0;
// no icon gap compensation
margin-left: 0;
}
&.key--is-index {
&.key-is-index {
// monospace for array indexes to avoid different widths
font-family: monospace;
}
Expand All @@ -153,7 +174,7 @@
transform: rotate(-90deg);
transition: transform ease-out var(--animation-time-normal);
&--expanded {
&.is-expandable {
transform: rotate(0);
}
}
Expand Down
77 changes: 44 additions & 33 deletions frontend/src/lib/components/common/TreeJsonValue.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
<script lang="ts">
import { Html } from "@dfinity/gix-components";
import type { TreeJsonValueType } from "$lib/utils/json.utils";
import { getTreeJsonValueRenderType } from "$lib/utils/json.utils";
import { stringifyJson } from "$lib/utils/utils.js";
import { splitE8sIntoChunks, stringifyJson } from "$lib/utils/utils.js";
import { i18n } from "$lib/stores/i18n";
// To avoid having quotes around all the value types
const formatData = (value: unknown) => {
const valueType = getTreeJsonValueRenderType(value);
const formatE8s = (data: unknown): string[] =>
splitE8sIntoChunks((data as Record<"e8s", unknown>)?.e8s);
const formatData = (value: unknown): string => {
if (valueType === "base64Encoding") {
return (data as { [key: string]: unknown })["base64Encoding"];
return (data as { [key: string]: unknown })["base64Encoding"] as string;
}
// TODO(max): think about moving to a separate component
if (valueType === "basisPoints") {
return `${(data as { [basisPoints: string]: unknown }).basisPoints}`;
}
if (valueType === "seconds") {
return `${(data as { [seconds: string]: unknown }).seconds}`;
}
if (
(
Expand All @@ -30,11 +38,9 @@
export let data: unknown | undefined = undefined;
export let key: string | undefined = undefined;
export let valueType: TreeJsonValueType;
let valueType: TreeJsonValueType;
$: valueType = getTreeJsonValueRenderType(data);
let value: unknown;
let value: string | undefined;
$: value = formatData(data);
let title: string | undefined;
Expand All @@ -46,41 +52,46 @@
<Html
text={`<img class="value ${valueType}" alt="${key}" src="${value}" loading="lazy" />`}
/>
{:else if valueType === "seconds"}
<span class="value {valueType}" {title}
>{value}
<span class="unit">{$i18n.proposal_detail.json_unit_seconds}</span>
</span>
{:else if valueType === "e8s"}
<span class="value {valueType}" {title}>
{#each formatE8s(data) as chunk}
<span>{chunk}</span>
{/each}
<span class="unit">{$i18n.proposal_detail.json_unit_e8s}</span>
</span>
{:else if valueType === "basisPoints"}
<span class="value {valueType}" {title}
>{value}
<span class="unit">{$i18n.proposal_detail.json_unit_basis_points}</span
></span
>
{:else}
<span class="value {valueType}" {title}>{value}</span>
{/if}

<style lang="scss">
@use "@dfinity/gix-components/dist/styles/mixins/fonts";
.key {
display: flex;
.value {
color: var(--description-color);
// for chunks and units
display: inline-flex;
align-items: center;
margin-right: var(--padding-2x);
gap: var(--padding-0_5x);
@include fonts.standard(true);
color: var(--content-color);
// keep lines (scroll horizontally)
white-space: nowrap;
&.root {
@include fonts.h4();
.unit {
margin-left: var(--padding-0_5x);
@include fonts.small(false);
}
&.key--expandable {
margin-right: 0;
// no icon gap compensation
margin-left: 0;
}
&.key--is-index {
// monospace for array indexes to avoid different widths
font-family: monospace;
}
}
.value {
// better shrink the value than the key
flex: 1 1 0;
// We want to break the value, so that the keys stay on the same line.
word-break: break-all;
color: var(--description-color);
}
// base64 encoded image
Expand Down
Loading

0 comments on commit d4a4d20

Please sign in to comment.