Packages
kino
0.9.0
0.19.0
0.18.0
0.17.0
0.16.1
0.16.0
0.15.3
0.15.2
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.2
0.13.1
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.0
0.6.2
0.6.1
0.6.0
retired
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Interactive widgets for Livebook
Current section
Files
Jump to
Current section
Files
lib/assets/debug_pipeline/main.js
export async function init(ctx, payload) {
await importJS(
"https://cdn.jsdelivr.net/npm/vue@3.2.37/dist/vue.global.prod.js"
);
await importJS(
"https://cdn.jsdelivr.net/npm/vue-dndrop@1.2.13/dist/vue-dndrop.min.js"
);
ctx.importCSS("main.css");
ctx.importCSS(
"https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap"
);
ctx.importCSS(
"https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.min.css"
);
const BaseSwitch = {
name: "BaseSwitch",
template: `
<label class="switch-button">
<input type="checkbox" v-bind="$attrs" class="switch-button__checkbox" />
<div class="switch-button__bg" />
</label>
`,
};
const app = Vue.createApp({
components: {
BaseSwitch,
Container: VueDndrop.Container,
Draggable: VueDndrop.Draggable,
},
template: `
<div class="app">
<div class="navigation">
<div class="headline">
<span class="dbg">dbg:{{ dbgLine }}</span>
<span>{{ dbgInfo }}</span>
</div>
<div class="actions">
<div class="copy-source" @click="copySource">
<span class="change-indicator" v-if="isChanged">
<span class="dot"></span>
<span>Copy new pipeline</span>
</span>
<button class="icon-button">
<i class="ri ri-clipboard-line" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<Container @drop="handleItemDrop">
<Draggable
v-for="(item, index) in items"
:key="item.id"
:drag-not-allowed="index === 0"
>
<div
class="box"
:class="{
selectable: isItemSelectable(item),
selected: item.id === selectedId,
errored: item.id === erroredId,
first: index === 0,
}"
@click="handleItemClick(item)"
>
<div class="primary">
<span class="source">{{ item.source }}</span>
<BaseSwitch
:checked="item.enabled"
@change="handleEnabledChange(item, $event)"
@click.stop
v-if="item !== items[0]"
/>
</div>
<div class="error" v-if="item.id === erroredId">
{{error}}
</div>
</div>
</Draggable>
</Container>
<div class="output-label">
Output:
</div>
</div>
`,
data() {
return {
dbgSameFile: payload.dbg_same_file,
dbgLine: payload.dbg_line,
callCount: payload.call_count,
items: payload.items,
selectedId: payload.selected_id,
erroredId: payload.errored_id,
error: payload.error,
isChanged: payload.changed,
};
},
computed: {
abortedIds() {
if (this.erroredId === null) {
return [];
} else {
const erroredIdx = this.items.findIndex(
(item) => item.id === this.erroredId
);
return this.items.slice(erroredIdx).map((item) => item.id);
}
},
dbgInfo() {
const info = [
!this.dbgSameFile && "from another cell",
this.callCount > 1 && `showing first out of ${this.callCount} calls`,
]
.filter((x) => x)
.join("; ");
if (info) {
return `(${info})`;
} else {
return null;
}
},
},
methods: {
isItemSelectable(item) {
return item.enabled && !this.abortedIds.includes(item.id);
},
handleItemClick(item) {
if (this.isItemSelectable(item)) {
ctx.pushEvent("select_item", { id: item.id });
}
},
handleEnabledChange(item, event) {
ctx.pushEvent("update_enabled", {
id: item.id,
enabled: event.target.checked,
});
},
handleItemDrop({ removedIndex, addedIndex }) {
addedIndex = Math.max(addedIndex, 1);
if (removedIndex === addedIndex) return;
const item = this.items[removedIndex];
ctx.pushEvent("move_item", { id: item.id, index: addedIndex });
},
copySource() {
const source = this.items
.filter((item) => item.enabled)
.map((item) => item.source)
.join("\n");
if ("clipboard" in navigator) {
navigator.clipboard.writeText(source);
} else {
alert(
"Sorry, your browser does not support clipboard copy.\nThis generally requires a secure origin — either HTTPS or localhost."
);
}
},
moveSelection(offset) {
const items = this.items.filter((item) => this.isItemSelectable(item));
const idx = items.findIndex((item) => item.id === this.selectedId);
const item = items[idx + offset];
if (item) {
ctx.pushEvent("select_item", { id: item.id });
}
},
},
created() {
window.addEventListener("keydown", (event) => {
if (event.key === "ArrowUp") {
this.moveSelection(-1);
event.preventDefault();
} else if (event.key === "ArrowDown") {
this.moveSelection(1);
event.preventDefault();
}
});
},
}).mount(ctx.root);
ctx.handleEvent("item_selected", ({ id }) => {
app.selectedId = id;
});
ctx.handleEvent("set_errored", ({ id, error, selected_id }) => {
app.erroredId = id;
app.error = error;
app.selectedId = selected_id;
});
ctx.handleEvent(
"enabled_updated",
({ id, enabled, selected_id, changed }) => {
const item = app.items.find((item) => item.id === id);
item.enabled = enabled;
app.selectedId = selected_id;
app.isChanged = changed;
}
);
ctx.handleEvent("item_moved", ({ id, index, changed }) => {
const currentIndex = app.items.findIndex((item) => item.id === id);
const item = app.items[currentIndex];
app.items.splice(currentIndex, 1);
app.items.splice(index, 0, item);
app.isChanged = changed;
});
ctx.handleEvent("call_count_updated", ({ call_count }) => {
app.callCount = call_count;
});
}
// Imports a JS script globally using a <script> tag
function importJS(url) {
return new Promise((resolve, reject) => {
const scriptEl = document.createElement("script");
scriptEl.addEventListener(
"load",
(event) => {
resolve();
},
{ once: true }
);
scriptEl.src = url;
document.head.appendChild(scriptEl);
});
}