Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions ui/app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -214,5 +214,7 @@ html, body {
}
}

/* .fnLink { @apply font-mono; } */
.unsafeFnLink { @apply text-red-700 dark:text-red-400 hover:font-bold hover:text-red-500 transition-colors; }
.unsafeFnLink {
@apply text-rose-700 hover:text-rose-500 font-bold transition-colors
dark:hover:text-rose-400 dark:hover:text-rose-300;
}
23 changes: 17 additions & 6 deletions ui/app/components/Flow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import { adtURL, type FlowOpts } from '~/lib/topbar';
import type { PanelContent } from '~/lib/panel';
import ELK from 'elkjs/lib/elk.bundled.js'
import { NodeKind, Plot, PlotConfig, type IdToItem } from '~/utils/graph';
import type { AdtOpts } from '~/lib/output/adt';
import type { AdtClicked, AdtOpts } from '~/lib/output/adt';
import { extractAdtFromIdField } from '~/lib/graph';

const flowOpts = defineModel<FlowOpts>('flowOpts', { required: true });
const adtOpts = defineModel<AdtOpts>('adtOpts', { required: true });
const adtClicked = defineModel<AdtClicked>('adtClicked', { required: true });
const panelContent = defineModel<PanelContent>('panelContent', { required: true });

const elk = new ELK()
Expand Down Expand Up @@ -57,17 +59,26 @@ const data = ref<Data>(EMPTY_DATA);

// Respond to node click.
onNodeClick(event => {
const selected = data.value.id_to_item[event.node.id]
if (selected && selected.kind === NodeKind.Adt) {
const id = event.node.id
const selected = data.value.id_to_item[id]
const selectAdt = selected?.kind === NodeKind.Adt
const selectField = selected?.kind === NodeKind.Field

if (selectAdt || selectField) {
const name = selected.name
const adt = selectField ? extractAdtFromIdField(id) : name

// Update adt data.
const url = adtURL(name)
const url = adtURL(adt)
$fetch(url)
.then(text => {
// Update nodeItem here because panel reacts to the two values.
panelContent.value.nodeItem = name
adtOpts.value = { name, data: JSON.parse(text as string) }
panelContent.value.nodeItem = adt
// Update adt data.
adtOpts.value = { name: adt, data: JSON.parse(text as string) }
// Update adt clicked.
const click = { clickedAdt: adt, clickedField: selectField ? name : undefined }
Object.assign(adtClicked.value, click)
})
.catch(err => console.log("Failed to fetch adt.json", err))
return
Expand Down
23 changes: 20 additions & 3 deletions ui/app/components/UPG.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
<div class="upg-left">
<WidgetTopBar v-model:flowOpts="flowOpts" v-model:crate="crate" v-model="nodeItem" v-model:share="share"
:tags="tags" :unsafeFns="unsafeFns" />
<Flow :nodeItem="nodeItem" :tags="tags" v-model:flowOpts="flowOpts" v-model:panelContent="panelContent"
v-model:adtOpts="adtOpts" />
<Flow :nodeItem="nodeItem" :tags="tags" :adtClicked="adtClicked" v-model:flowOpts="flowOpts"
v-model:panelContent="panelContent" v-model:adtOpts="adtOpts" />
<UModal :ui="{ content: 'w-[76vw] max-w-none' }" v-model:open="adtClicked.open">
<template #content>
<CodeAdtPopup :adt="adtOpts.data" :tags="tags" :unsafeFns="unsafeFns" :adtClicked="adtClicked" />
</template>
</UModal>
</div>
<div class="upg-right">
<div class="upg-panel upg-panel-1">
Expand All @@ -22,7 +27,7 @@ import type { FlowOpts, UnsafeFns } from "~/lib/topbar"
import { type DataTags, } from '~/lib/output/tag';
import { Panel, toPanel, toPanelStr, type PanelContent } from "~/lib/panel"
import { Crate, FLOW_OPTS, defaultCrateItemQuery, tagURL, toCrate, toViewTypes, unsafeFnsURL } from "~/lib/topbar";
import type { AdtOpts } from "~/lib/output/adt";
import type { AdtClicked, AdtOpts } from "~/lib/output/adt";

const router = useRouter();
const route = useRoute();
Expand Down Expand Up @@ -85,6 +90,18 @@ const downPanel = ref(initState.down);
const flowOpts = ref<FlowOpts>(initState.flowOpts);
const adtOpts = ref<AdtOpts>({});

const adtClicked = ref<AdtClicked>({ open: false })
watch(() => ({
isAdtPanel: upPanel.value === Panel.Adt || downPanel.value === Panel.Adt,
isClicked: adtClicked.value.clickedAdt || adtClicked.value.clickedField,
adt: adtClicked.value
}), ({ isAdtPanel, isClicked, adt }) => {
// Auto open adt panel when side panels doesn't show adt panel, and user clicked adt or field.
if (!isAdtPanel && isClicked) {
adtClicked.value = { open: true, lastClickedAdt: adt.clickedAdt, lastClickedField: adt.clickedField }
}
})

const share = ref<boolean>(false)
watch(share, val => {
if (!val) return;
Expand Down
262 changes: 262 additions & 0 deletions ui/app/components/code/AdtPopup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
<template>
<div class="min-h-[75vh] flex">
<UTree :items="items" :get-key="item => item.id" v-model="active" class="w-50 h-full"
expanded-icon="tabler:square-letter-f" />

<div class="w-full">
<WidgetSearchFn :v_fn="adtItem.v_fn" :unsafeFns="unsafeFns" v-model="search" :title="adtItem.desc" />
</div>
</div>
</template>

<script setup lang="ts">
import type { AdtClicked, AdtPanelItem, DataAdt } from '~/lib/output/adt';
import { getTag, type DataTags } from '~/lib/output/tag';
import type { Search, UnsafeFns } from '~/lib/topbar';
import type { TreeItem } from '@nuxt/ui'

const props = defineProps<{ adt?: DataAdt, tags: DataTags, unsafeFns: UnsafeFns, adtClicked: AdtClicked }>()

function gen(v: string[] | undefined, kind: string, desc: string) {
const v_fn = v ? v.map(
name => ({ name, tags: getTag(name, props.tags, true) }))
: []
return { v_fn, kind, desc }
}

// Fields
const fields = computed<string[]>(() => {
return Object.values(props.adt?.variant_fields ?? {}).map(f => f.name)
})

const fieldAccess = computed<{ field: string, data: AdtPanelItem }[]>(() => {
const adt = props.adt
if (adt === undefined) return []
const fields_ = fields.value
if (adt.access_field.length != fields_.length) return []

return fields_.flatMap((field, idx) => {
const access = adt!.access_field[idx]!
return [
{ field, data: gen(access.read, "Read", `Read Access to Field \`${field}\` of \`${adt.name}\``) },
{ field, data: gen(access.write, "Write", `Write Access to Field \`${field}\` of \`${adt.name}\``) },
// { field, data: gen(access.other, "Unknown", `Other Access to Field \`${field}\` of Self`) },
]
})
})

const groupedFieldAccess = computed<{ [key: string]: AdtPanelItem[] }>(() => {
const map: { [key: string]: AdtPanelItem[] } = {}
fieldAccess.value.forEach(({ field, data }) => {
map[field] ??= []
map[field].push(data)
})
return map
})

// Constructors
const constructors = computed<AdtPanelItem>(() =>
gen(props.adt?.constructors, "Constructors", `All Constructors of \`${props.adt?.name ?? ''}\``)
)

// As arguments
const group_access_self_as_arg = computed<{ group: string, data: AdtPanelItem[] }>(() => {
const adt = props.adt
if (!adt) return { group: "", data: [] }

const group = adt.access_self_as_arg
return {
group: "Access Adt through arguments:", data: [
gen(group.read, "Read", `Read Access to \`${adt.name}\` through a Function Argument`),
gen(group.write, "Write", `Write Access to \`${adt.name}\` through a Function Argument`),
// gen(group.other, "Unknown", "Other Access to Self through a function argument"),
]
}
})

// As locals
const group_access_self_as_locals = computed<{ group: string, data: AdtPanelItem[] }>(() => {
const adt = props.adt
if (!adt) return { group: "", data: [] }

const group = adt.access_self_as_locals
return {
group: "Access Adt through locals:", data: [
gen(group.read, "Read", `Read Access to \`${adt.name}\` through a Function Local`),
gen(group.write, "Write", `Write Access to \`${adt.name}\` through a Function Local`),
// gen(group.other, "Unknown", "Other Access to Self through a function local"),
]
}
})

const active = ref<TreeItem>()
const adtItem = ref<AdtPanelItem>({ v_fn: [], kind: "", desc: "" })

// This is used to determine which subpanel is selected as per the click,
enum Selected { Field, Constructor, AsArg, AsLocals }

// The default selected item panel (write).
const initAdtItem = () => {
let item: AdtPanelItem | undefined = undefined
let selected: Selected | undefined = undefined
let selectedIdx: string | number | undefined = undefined

const clicked = props.adtClicked
const field = clicked.lastClickedField
const fields = groupedFieldAccess.value
if (field) {
// field name in fn json includes the idx prefix like `0-fieldName`,
// while adt json only uses `fieldName` directly.
const fieldName = field.match(/[^-]+$/)?.[0]
item = fields[field]?.at(-1) || (fieldName ? fields[fieldName]?.at(-1) : undefined)
if (item) {
selected = Selected.Field
selectedIdx = fieldName
}
} else if (clicked.lastClickedAdt) {
if (constructors.value.v_fn.length) {
item = constructors.value
selected = Selected.Constructor
} else {
item = Object.values(fields)[0]?.at(-1)
if (item) {
selected = Selected.Field
// Default to write access
selectedIdx = -1
} else {
item = group_access_self_as_arg.value.data.at(-1)
if (item) {
selected = Selected.AsArg
selectedIdx = -1
} else {
item = group_access_self_as_locals.value.data.at(-1)
if (item) {
selected = Selected.AsLocals
selectedIdx = -1
}
}
}
}
}

if (item) return { item, selected, selectedIdx }
}

const search = ref<Search>({ withTags: false, unsafeOnly: true, text: "", page: 1, itemsPerPage: 20 })

const items = computed<TreeItem[]>(() => {
const tree: TreeItem[] = []

const initItem = initAdtItem()
let selected: Selected | undefined = undefined
let selectedIdx = undefined
if (initItem) {
// We must explicitly set adtItem, because active doesn't trigger onSelect.
adtItem.value = initItem.item
selected = initItem.selected
selectedIdx = initItem.selectedIdx
}

const lenConstructor = constructors.value.v_fn.length
if (lenConstructor) {
const label = kindLabel(constructors.value)
const node = {
label, id: label, onSelect: () => adtItem.value = constructors.value,
icon: "tabler:hexagon-letter-c-filled",
}
tree.push(node)
if (selected === Selected.Constructor) {
active.value = node
selected = undefined
}
}

const fields = Object.entries(groupedFieldAccess.value)
if (fields.length) {
const fieldHeader: TreeItem = {
label: (fields.length === 1) ? "Access Field" : "Access Fields",
id: "Field", defaultExpanded: true, children: []
}
tree.push(fieldHeader)

for (const [field, v_data] of fields) {
const fieldItem: TreeItem = {
label: field, id: `Field@${field}`, defaultExpanded: true, children: [],
icon: "tabler:hexagon-letter-f-filled",
}
for (const data of v_data) {
const label = kindLabel(data)
const node = {
label, id: `Field@${field}@kind@${data.kind}`,
onSelect: () => adtItem.value = data
}
fieldItem.children!.push(node)
}

fieldHeader.children!.push(fieldItem)
}
if (selected === Selected.Field) {
if (typeof selectedIdx === "string") {
const fieldName = selectedIdx
selectedIdx = fields.findIndex(([f, _]) => f === fieldName)
if (selectedIdx === -1) console.log(`${fieldName} is not a valid field in`, fields)
}
active.value = fieldHeader.children!.at(selectedIdx as number)!.children!.at(-1)
}
}

const as_arg = group_access_self_as_arg.value.data
const as_local = group_access_self_as_locals.value.data
if (as_arg.length && as_local.length) {
const headerLabel = "Access Self"
const header: TreeItem = {
label: headerLabel, id: headerLabel, defaultExpanded: true, children: []
}
tree.push(header)

if (as_arg.length) {
const argLabel = "As Arguments"
const argHeader: TreeItem = {
label: argLabel, id: argLabel, defaultExpanded: true, children: [],
icon: "tabler:hexagon-letter-a-filled",
}
header.children!.push(argHeader)

for (const data of as_arg) {
const node = {
label: kindLabel(data), id: `AsArg@${data.kind}`,
onSelect: () => adtItem.value = data
}
argHeader.children!.push(node)
}
if (selected === Selected.AsArg) {
active.value = argHeader.children!.at(selectedIdx as number)
}
}

if (as_local.length) {
const localsLabel = "As Locals"
const localsHeader: TreeItem = {
label: localsLabel, id: localsLabel, defaultExpanded: true, children: [],
icon: "tabler:hexagon-letter-l-filled",
}
header.children!.push(localsHeader)

for (const data of as_local) {
const node = {
label: kindLabel(data), id: `AsLocal@${data.kind}`,
onSelect: () => adtItem.value = data
}
localsHeader.children!.push(node)
}
if (selected === Selected.AsLocals) {
active.value = localsHeader.children!.at(selectedIdx as number)
}
}
}

return tree
})

const kindLabel = (item: AdtPanelItem) => `${item.kind} (${item.v_fn.length})`
</script>
4 changes: 4 additions & 0 deletions ui/app/lib/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export function idField(adt: string, field: string) {
return `field@${field}@adt@${adt}`
}

export function extractAdtFromIdField(id: string) {
return id.match(/adt@([^@]+)$/)?.[1] ?? ""
}

export function isAdtID(id: string) {
return id.startsWith("adt@")
}
Expand Down
9 changes: 5 additions & 4 deletions ui/app/lib/output/adt.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { SearchFnItem } from "../topbar"

export type AdtOpts = {
name?: string, data?: DataAdt,
}
export type AdtOpts = { name?: string, data?: DataAdt }

export const ADT_OPTs: AdtOpts = {}
export type AdtClicked = {
open: boolean, clickedAdt?: string, clickedField?: string,
lastClickedAdt?: string, lastClickedField?: string,
}

export type DataAdt = {
name: string,
Expand Down
Loading