Skip to content

Commit

Permalink
feat: allow for pinning scenarios/datasets in model-check report (#551)
Browse files Browse the repository at this point in the history
Fixes #537
  • Loading branch information
chrispcampbell authored Oct 11, 2024
1 parent 4f0495a commit e3b0463
Show file tree
Hide file tree
Showing 24 changed files with 855 additions and 206 deletions.
3 changes: 2 additions & 1 deletion packages/check-ui-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"assert-never": "^1.2.1",
"chart.js": "^2.9.4",
"copy-text-to-clipboard": "^3.2.0",
"fontfaceobserver": "^2.3.0"
"fontfaceobserver": "^2.3.0",
"svelte-dnd-action": "^0.9.50"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.2",
Expand Down
5 changes: 3 additions & 2 deletions packages/check-ui-shell/src/_shared/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Writable } from 'svelte/store'
import { writable } from 'svelte/store'

/**
* Return a Svelte writable store that is backed by local storage.
* Return a Svelte writable number store that is backed by local storage.
*/
export function localStorageWritableNumber(key: string, defaultValue: number): Writable<number> {
const initialStringValue = localStorage.getItem(key)
Expand Down Expand Up @@ -37,9 +37,10 @@ export function localStorageWritableNumber(key: string, defaultValue: number): W
}

/**
* Return a Svelte writable store that is backed by local storage.
* Return a Svelte writable boolean store that is backed by local storage.
*/
export function localStorageWritableBoolean(key: string, defaultValue: boolean): Writable<boolean> {
// TODO: Update callers to use `localStore` and then remove this variant
const initialStringValue = localStorage.getItem(key)
let initialValue: boolean
if (initialStringValue !== null) {
Expand Down
75 changes: 50 additions & 25 deletions packages/check-ui-shell/src/app-vm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) 2021-2022 Climate Interactive / New Venture Fund

import assertNever from 'assert-never'

import type { Readable, Writable } from 'svelte/store'
import { get, writable } from 'svelte/store'

Expand All @@ -12,17 +14,18 @@ import type { UserPrefs } from './_shared/user-prefs'
import type { AppModel } from './model/app-model'

import type { ComparisonGroupingKind } from './components/compare/_shared/comparison-grouping-kind'
import type { PinnedItemState, PinnedItemStates } from './components/compare/_shared/pinned-item-state'
import { createPinnedItemStates } from './components/compare/_shared/pinned-item-state'
import type { CompareDetailViewModel } from './components/compare/detail/compare-detail-vm'
import { createCompareDetailViewModel } from './components/compare/detail/compare-detail-vm'
import type { ComparisonSummaryRowViewModel } from './components/compare/summary/comparison-summary-row-vm'
import type { ComparisonSummaryViewModel } from './components/compare/summary/comparison-summary-vm'
import type { HeaderViewModel } from './components/header/header-vm'
import { createHeaderViewModel } from './components/header/header-vm'
import type { PerfViewModel } from './components/perf/perf-vm'
import { createPerfViewModel } from './components/perf/perf-vm'
import type { SummaryViewModel } from './components/summary/summary-vm'
import { createSummaryViewModel } from './components/summary/summary-vm'
import assertNever from 'assert-never'
import type { ComparisonSummaryViewModel } from './components/compare/summary/comparison-summary-vm'

export interface RunSuiteCallbacks {
onProgress?: (pct: number) => void
Expand All @@ -36,6 +39,7 @@ export class AppViewModel {
public readonly progress: Readable<string>
public readonly userPrefs: UserPrefs
public readonly headerViewModel: HeaderViewModel
private readonly pinnedItemStates: PinnedItemStates
public summaryViewModel: SummaryViewModel
private cancelRunSuite: () => void

Expand Down Expand Up @@ -69,6 +73,9 @@ export class AppViewModel {

// Create the header view model
this.headerViewModel = createHeaderViewModel(appModel.config.comparison, simplifyScenarios, zoom, consistentYRange)

// Create the object that manages pinned items states
this.pinnedItemStates = createPinnedItemStates()
}

runTestSuite(): void {
Expand All @@ -94,7 +101,8 @@ export class AppViewModel {
this.appModel.checkDataCoordinator,
checkReport,
comparisonConfig,
comparisonSummary
comparisonSummary,
this.pinnedItemStates
)
this.writableChecksInProgress.set(false)
} else {
Expand All @@ -121,7 +129,8 @@ export class AppViewModel {
this.appModel.checkDataCoordinator,
checkReport,
comparisonConfig,
comparisonSummary
comparisonSummary,
this.pinnedItemStates
)
this.writableChecksInProgress.set(false)
},
Expand All @@ -140,46 +149,62 @@ export class AppViewModel {
createCompareDetailViewModelForSummaryRow(
summaryRowViewModel: ComparisonSummaryRowViewModel
): CompareDetailViewModel {
const comparisonSummaryViewModel = this.getComparisonSummaryViewModel(summaryRowViewModel.kind)
const groupSummary = summaryRowViewModel.groupSummary
const groupKey = summaryRowViewModel.groupKey

const viewGroup = summaryRowViewModel.viewMetadata?.viewGroup
const view = summaryRowViewModel.viewMetadata?.view

// Determine which rows precede and follow the selected row
let previousRowIndex: number
let nextRowIndex: number
const rowCount = comparisonSummaryViewModel.allRows.length
const rowIndex = comparisonSummaryViewModel.allRows.findIndex(row => row.groupKey === groupKey)
if (rowIndex >= 0) {
if (rowIndex > 0) {
previousRowIndex = rowIndex - 1
}
if (rowIndex < rowCount - 1) {
nextRowIndex = rowIndex + 1
}
let pinnedItemState: PinnedItemState
if (groupSummary.group.kind === 'by-dataset') {
// Show pinned scenarios at the top of the detail view
pinnedItemState = this.pinnedItemStates.pinnedScenarios
} else {
// Show pinned datasets at the top of the detail view
pinnedItemState = this.pinnedItemStates.pinnedDatasets
}

return createCompareDetailViewModel(
summaryRowViewModel.key,
this.appModel.config.comparison,
this.appModel.comparisonDataCoordinator,
this.userPrefs,
groupSummary,
viewGroup,
view,
previousRowIndex,
nextRowIndex
pinnedItemState
)
}

createCompareDetailViewModelForSummaryRowIndex(
createCompareDetailViewModelForFirstSummaryRow(kind: ComparisonGroupingKind): CompareDetailViewModel | undefined {
// Get the index of the associated row in the context of the summary view
const comparisonSummaryViewModel = this.getComparisonSummaryViewModel(kind)
const allRows = get(comparisonSummaryViewModel.allRows)
if (allRows.length > 0) {
// Create a detail view for the first row
const firstRow = allRows[0]
return this.createCompareDetailViewModelForSummaryRow(firstRow)
} else {
return undefined
}
}

createCompareDetailViewModelForSummaryRowWithDelta(
kind: ComparisonGroupingKind,
rowIndex: number
): CompareDetailViewModel {
summaryRowKey: string,
delta: -1 | 1
): CompareDetailViewModel | undefined {
// Get the index of the associated row in the context of the summary view
const comparisonSummaryViewModel = this.getComparisonSummaryViewModel(kind)
const rowViewModel = comparisonSummaryViewModel.allRows[rowIndex]
return this.createCompareDetailViewModelForSummaryRow(rowViewModel)
const allRows = get(comparisonSummaryViewModel.allRows)
const rowIndex = allRows.findIndex(row => row.key === summaryRowKey)
const adjRowIndex = rowIndex + delta
if (adjRowIndex >= 0 && adjRowIndex < allRows.length) {
// Create a detail view for the adjacent row
const prevRow = allRows[adjRowIndex]
return this.createCompareDetailViewModelForSummaryRow(prevRow)
} else {
return undefined
}
}

private getComparisonSummaryViewModel(kind: ComparisonGroupingKind): ComparisonSummaryViewModel {
Expand Down
19 changes: 14 additions & 5 deletions packages/check-ui-shell/src/app.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,27 @@ function onCommand(event: CustomEvent) {
default:
return
}
compareDetailViewModel = viewModel.createCompareDetailViewModelForSummaryRowIndex(kind, 0)
viewMode = 'comparison-detail'
const first = viewModel.createCompareDetailViewModelForFirstSummaryRow(kind)
if (first) {
compareDetailViewModel = first
viewMode = 'comparison-detail'
}
}
break
case 'show-comparison-detail':
compareDetailViewModel = viewModel.createCompareDetailViewModelForSummaryRow(cmdObj.summaryRow)
viewMode = 'comparison-detail'
break
case 'show-comparison-detail-at-index':
compareDetailViewModel = viewModel.createCompareDetailViewModelForSummaryRowIndex(cmdObj.kind, cmdObj.index)
viewMode = 'comparison-detail'
case 'show-comparison-detail-for-previous':
case 'show-comparison-detail-for-next': {
const delta = cmd === 'show-comparison-detail-for-previous' ? -1 : +1
const adjacent = viewModel.createCompareDetailViewModelForSummaryRowWithDelta(cmdObj.kind, cmdObj.summaryRowKey, delta)
if (adjacent) {
compareDetailViewModel = adjacent
viewMode = 'comparison-detail'
}
break
}
case 'show-perf':
if (!perfViewModel) {
perfViewModel = viewModel.createPerfViewModel()
Expand Down
22 changes: 22 additions & 0 deletions packages/check-ui-shell/src/components/_shared/click-outside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2024 Climate Interactive / New Venture Fund. All rights reserved.

/** Dispatch an event when click event occurs outside of the given element. */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function clickOutside(element: HTMLElement) {
// This implementation is based on:
// https://svelte.dev/repl/0ace7a508bd843b798ae599940a91783?version=3.16.7

const handleClick = (event: Event) => {
if (element && !element.contains(event.target as HTMLElement) && !event.defaultPrevented) {
element.dispatchEvent(new CustomEvent('clickout'))
}
}

document.addEventListener('click', handleClick, true)

return {
destroy() {
document.removeEventListener('click', handleClick, true)
}
}
}
125 changes: 125 additions & 0 deletions packages/check-ui-shell/src/components/_shared/context-menu.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<!-- Copyright (c) 2024 Climate Interactive / New Venture Fund -->

<!--
Based on example code by @dukenmarga from:
https://svelte.dev/repl/6fb90919e24942b2b47d9ad154386b0c?version=3.49.0
-->

<script context="module" lang="ts">
export interface ContextMenuItem {
key: string
displayText: string
iconClass?: string
disabled?: boolean
}
</script>

<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { clickOutside } from './click-outside'
export let items: ContextMenuItem[]
export let parentElem: HTMLElement
export let initialEvent: MouseEvent
const dispatch = createEventDispatcher()
// Cursor position of the initial event
let pos = { x: 0, y: 0 }
// Whether to show or hide the menu
let showMenu = false
$: if (initialEvent) {
const bounds = parentElem.getBoundingClientRect()
pos = {
x: initialEvent.clientX - bounds.left,
y: initialEvent.clientY - bounds.top
}
showMenu = true
} else {
showMenu = false
}
function onItemSelected(cmd: string) {
dispatch('item-selected', cmd)
}
</script>

{#if showMenu}
<nav use:clickOutside on:clickout style="position: absolute; top:{pos.y}px; left:{pos.x}px">
<div class="navbar" id="navbar">
<ul>
{#each items as item}
{#if item.key == "hr"}
<hr>
{:else}
<li><button class:disabled={item.disabled === true} on:click={() => onItemSelected(item.key)}><i class={item.iconClass}></i>{item.displayText}</button></li>
{/if}
{/each}
</ul>
</div>
</nav>
{/if}

<style lang="sass">
*
padding: 0
margin: 0
nav
z-index: 200
.navbar
display: inline-flex
flex-direction: column
width: 170px
background-color: #fff
border-radius: .4rem
box-shadow: 0 .2rem .4rem rgba(0,0,0,.8)
overflow: hidden
.navbar ul
margin: 6px
ul li
display: block
list-style-type: none
width: 1fr
ul li button
font-family: Roboto, sans-serif
font-weight: 700
font-size: 1rem
color: #222
width: 100%
height: 30px
text-align: left
border: 0px
background-color: #fff
ul li button.disabled
color: #888
pointer-events: none
ul li button:not(.disabled):hover
color: #000
text-align: left
border-radius: .3rem
background-color: #eee
ul li button i
padding: 0px 15px 0px 10px
hr
border: none
border-bottom: 1px solid #ccc
margin: 5px 0px
</style>
Loading

0 comments on commit e3b0463

Please sign in to comment.