Skip to content

Commit

Permalink
Display component evaluating status (#12126)
Browse files Browse the repository at this point in the history
* Display component evaluating status

* Review
  • Loading branch information
kazcw authored Jan 27, 2025
1 parent db2b784 commit c54fb07
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 28 deletions.
10 changes: 10 additions & 0 deletions app/gui/integration-test/project-view/graphRenderNodes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { test } from '@playwright/test'
import * as actions from './actions'
import { expect } from './customExpect'
import { mockExpressionUpdate } from './expressionUpdates'
import * as locate from './locate'

test('graph can open and render nodes', async ({ page }) => {
Expand All @@ -16,3 +17,12 @@ test('graph can open and render nodes', async ({ page }) => {
const finalNode = locate.graphNodeByBinding(page, 'final')
await expect(finalNode.locator('.WidgetToken')).toHaveText(['Main', '.', 'func1', 'prod'])
})

test('Component icon indicates evaluation in progress', async ({ page }) => {
await actions.goToGraph(page)

const node = locate.graphNodeByBinding(page, 'final')
await expect(node.locator('.WidgetIcon .LoadingSpinner')).not.toBeVisible()
await mockExpressionUpdate(page, 'final', { payload: { type: 'Pending', progress: 0.1 } })
await expect(node.locator('.WidgetIcon .LoadingSpinner')).toBeVisible()
})
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script setup lang="ts">
import { DisplayIcon } from '@/components/GraphEditor/widgets/WidgetIcon.vue'
import WidgetTreeRoot from '@/components/GraphEditor/WidgetTreeRoot.vue'
import { injectGraphSelection } from '@/providers/graphSelection'
import { WidgetInput, type WidgetUpdate } from '@/providers/widgetRegistry'
import { WidgetEditHandlerParent } from '@/providers/widgetRegistry/editHandler'
import { useGraphStore, type NodeId } from '@/stores/graph'
import type { NodeType } from '@/stores/graph/graphDatabase'
import { type NodeType } from '@/stores/graph/graphDatabase'
import { Ast } from '@/util/ast'
import { iconOfNode } from '@/util/getIconName'
import { computed } from 'vue'
import { DisplayIcon } from './widgets/WidgetIcon.vue'
import WidgetTreeRoot from './WidgetTreeRoot.vue'
import { iconOfNode, useDisplayedIcon } from '@/util/getIconName'
import { computed, toRef } from 'vue'
const props = defineProps<{
ast: Ast.Expression
Expand All @@ -20,7 +20,13 @@ const props = defineProps<{
conditionalPorts: Set<Ast.AstId>
extended: boolean
}>()
const graph = useGraphStore()
const selection = injectGraphSelection()
const baseIcon = computed(() => iconOfNode(props.nodeId, graph.db))
const { displayedIcon } = useDisplayedIcon(graph.db, toRef(props, 'nodeId'), baseIcon)
const rootPort = computed(() => {
const input = WidgetInput.FromAst(props.ast)
if (
Expand All @@ -30,15 +36,14 @@ const rootPort = computed(() => {
input.forcePort = true
}
if (!props.potentialSelfArgumentId && topLevelIcon.value) {
if (!props.potentialSelfArgumentId) {
input[DisplayIcon] = {
icon: topLevelIcon.value,
icon: displayedIcon.value,
showContents: props.nodeType != 'output',
}
}
return input
})
const selection = injectGraphSelection()
function selectNode() {
selection.setSelection(new Set([props.nodeId]))
Expand Down Expand Up @@ -77,8 +82,6 @@ function handleWidgetUpdates(update: WidgetUpdate) {
return true
}
const topLevelIcon = computed(() => iconOfNode(props.nodeId, graph.db))
function onCurrentEditChange(currentEdit: WidgetEditHandlerParent | undefined) {
if (currentEdit) selectNode()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
<script setup lang="ts">
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
import LoadingSpinner from '@/components/shared/LoadingSpinner.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
import type { URLString } from '@/util/data/urlString'
import type { Icon } from '@/util/iconMetadata/iconName'
import NodeWidget from '../NodeWidget.vue'
import { type URLString } from '@/util/data/urlString'
import { type Icon } from '@/util/iconMetadata/iconName'
import { computed } from 'vue'
const props = defineProps(widgetProps(widgetDefinition))
const icon = computed(() => props.input[DisplayIcon].icon)
</script>

<script lang="ts">
export const DisplayIcon: unique symbol = Symbol.for('WidgetInput:DisplayIcon')
declare module '@/providers/widgetRegistry' {
export interface WidgetInput {
[DisplayIcon]?: {
icon: Icon | URLString
icon: Icon | URLString | '$evaluating'
allowChoice?: boolean
showContents?: boolean
}
Expand All @@ -32,7 +36,16 @@ export const widgetDefinition = defineWidget(

<template>
<div class="WidgetIcon">
<SvgIcon class="nodeCategoryIcon grab-handle" :name="props.input[DisplayIcon].icon" />
<div class="iconContainer">
<Transition>
<LoadingSpinner
v-if="icon === '$evaluating'"
class="nodeCategoryIcon grab-handle"
:size="16"
/>
<SvgIcon v-else class="nodeCategoryIcon grab-handle" :name="icon" />
</Transition>
</div>
<NodeWidget v-if="props.input[DisplayIcon].showContents === true" :input="props.input" />
</div>
</template>
Expand All @@ -43,10 +56,33 @@ export const widgetDefinition = defineWidget(
flex-direction: row;
align-items: center;
gap: var(--widget-token-pad-unit);
> .SvgIcon {
margin: 0 calc((var(--node-port-height) - 16px) / 2);
display: flex;
}
.iconContainer {
position: relative;
height: 16px;
width: 16px;
margin: 0 calc((var(--node-port-height) - 16px) / 2);
}
.nodeCategoryIcon {
position: absolute;
}
.LoadingSpinner {
border: 4px solid;
border-radius: 100%;
border-color: rgba(255, 255, 255, 90%) #0000;
animation: s1 0.8s infinite;
}
@keyframes s1 {
to {
transform: rotate(0.5turn);
}
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.1s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
<script setup lang="ts">
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
import { DisplayIcon } from '@/components/GraphEditor/widgets/WidgetIcon.vue'
import { injectFunctionInfo } from '@/providers/functionInfo'
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
import { injectWidgetTree } from '@/providers/widgetTree'
import { useGraphStore } from '@/stores/graph'
import { Ast } from '@/util/ast'
import { displayedIconOf } from '@/util/getIconName'
import { computed } from 'vue'
import { DisplayIcon } from './WidgetIcon.vue'
import { displayedIconOf, useDisplayedIcon } from '@/util/getIconName'
import { computed, toRef } from 'vue'
const props = defineProps(widgetProps(widgetDefinition))
const functionInfo = injectFunctionInfo(true)
const graph = useGraphStore()
const tree = injectWidgetTree()
const displayedIcon = computed(() => {
const baseIcon = computed(() => {
const callInfo = functionInfo?.callInfo
return displayedIconOf(
callInfo?.suggestion,
callInfo?.methodCall.methodPointer,
functionInfo?.outputType ?? 'Unknown',
)
})
const { displayedIcon } = useDisplayedIcon(graph.db, toRef(tree, 'externalId'), baseIcon)
const iconInput = computed(() => {
const lhs = props.input.value.lhs
if (!lhs) return
const input = WidgetInput.WithPort(WidgetInput.FromAst(lhs))
const icon = displayedIcon.value
if (icon) input[DisplayIcon] = { icon, showContents: showFullAccessChain.value }
input[DisplayIcon] = { icon: displayedIcon.value, showContents: showFullAccessChain.value }
return input
})
Expand Down
28 changes: 25 additions & 3 deletions app/gui/src/project-view/util/getIconName.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { NodeId } from '@/stores/graph'
import { GraphDb } from '@/stores/graph/graphDatabase'
import { type NodeId } from '@/stores/graph'
import { type GraphDb } from '@/stores/graph/graphDatabase'
import {
SuggestionKind,
type SuggestionEntry,
type Typename,
} from '@/stores/suggestionDatabase/entry'
import type { Icon } from '@/util/iconMetadata/iconName'
import { type URLString } from '@/util/data/urlString'
import { type Icon } from '@/util/iconMetadata/iconName'
import { type MethodPointer } from '@/util/methodPointer'
import { type ToValue } from '@/util/reactivity'
import { computed, toValue } from 'vue'
import { type ExternalId } from 'ydoc-shared/yjsModel'

const typeNameToIconLookup: Record<string, Icon> = {
'Standard.Base.Data.Text.Text': 'text_input',
Expand Down Expand Up @@ -68,3 +72,21 @@ export function iconOfNode(node: NodeId, graphDb: GraphDb) {
return 'data_input'
}
}

/**
* Returns the icon to show on a component, using either the provided base icon or an icon representing its current
* status.
*/
export function useDisplayedIcon(
graphDb: GraphDb,
externalId: ToValue<ExternalId>,
baseIcon: ToValue<Icon | URLString>,
) {
const evaluating = computed(() => {
const payload = graphDb.getExpressionInfo(toValue(externalId))?.payload
return payload?.type === 'Pending' && payload.progress
})
return {
displayedIcon: computed(() => (evaluating.value ? '$evaluating' : toValue(baseIcon))),
}
}

0 comments on commit c54fb07

Please sign in to comment.