Skip to content

Commit

Permalink
widgets: Add new "DoItYourself" widget
Browse files Browse the repository at this point in the history
This is a widget that runs bare Javascript code created by the user. It does nothing this the user writes the code.

The idea with this widget is to allow people to add any type of functionality to Cockpit without actually knowing the Cockpit codebase previously.

We expect a lot of cool stuff to come from this!
  • Loading branch information
rafaellehmkuhl committed Jan 3, 2025
1 parent 92a289b commit 063fe88
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 0 deletions.
Binary file added src/assets/widgets/DoItYourself.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/components/EditMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ import CompassImg from '@/assets/widgets/Compass.png'
import CompassHUDImg from '@/assets/widgets/CompassHUD.png'
import CustomWidgetBaseImg from '@/assets/widgets/CustomWidgetBase.png'
import DepthHUDImg from '@/assets/widgets/DepthHUD.png'
import DoItYourselfImg from '@/assets/widgets/DoItYourself.png'
import IFrameImg from '@/assets/widgets/IFrame.png'
import ImageViewImg from '@/assets/widgets/ImageView.png'
import MapImg from '@/assets/widgets/Map.png'
Expand Down Expand Up @@ -804,6 +805,7 @@ const widgetImages = {
CompassHUD: CompassHUDImg,
CustomWidgetBase: CustomWidgetBaseImg,
DepthHUD: DepthHUDImg,
DoItYourself: DoItYourselfImg,
IFrame: IFrameImg,
ImageView: ImageViewImg,
Map: MapImg,
Expand Down
242 changes: 242 additions & 0 deletions src/components/widgets/DoItYourself.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<!-- eslint-disable no-useless-escape -->
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="main">
<div class="w-full h-full" v-html="compiledCode" />
</div>
<v-dialog
v-model="widgetStore.widgetManagerVars(widget.hash).configMenuOpen"
:max-width="interfaceStore.isOnSmallScreen ? '100%' : '800px'"
@after-enter="handleDialogOpen"
@after-leave="handleDialogClose"
>
<vue-draggable-resizable :drag-handle="'.drag-handle'" w="auto" h="auto" :handles="['tm', 'mr', 'bm', 'ml']">
<v-card class="pa-2" :style="interfaceStore.globalGlassMenuStyles">
<v-card-title>
<v-icon class="drag-handle">mdi-drag</v-icon>
Do It Yourself widget configuration
</v-card-title>
<v-card-text>
<div ref="htmlEditorContainer" class="editor-container" />
<div ref="cssEditorContainer" class="editor-container" />
<div ref="jsEditorContainer" class="editor-container" />
</v-card-text>
<v-card-actions>
<div class="flex justify-between items-center pa-2 w-full h-full">
<v-btn color="white" variant="text" @click="closeDialog">Close</v-btn>
<div class="flex gap-x-10">
<v-btn variant="text" @click="resetChanges">Reset</v-btn>
<v-btn color="primary" variant="text" @click="applyChanges">Apply</v-btn>
</div>
</div>
</v-card-actions>
</v-card>
</vue-draggable-resizable>
</v-dialog>
</template>

<script setup lang="ts">
import * as monaco from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import { computed, onBeforeMount, onBeforeUnmount, onMounted, ref, toRefs } from 'vue'

import { useAppInterfaceStore } from '@/stores/appInterface'
import { useWidgetManagerStore } from '@/stores/widgetManager'
import type { Widget } from '@/types/widgets'

self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new jsonWorker()
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new cssWorker()
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return new htmlWorker()
}
if (label === 'typescript' || label === 'javascript') {
return new tsWorker()
}
return new editorWorker()
},
}

const interfaceStore = useAppInterfaceStore()
const widgetStore = useWidgetManagerStore()

const props = defineProps<{
/**
* Widget reference
*/
widget: Widget
}>()

const widget = toRefs(props).widget
const htmlEditorContainer = ref<HTMLElement | null>(null)
const cssEditorContainer = ref<HTMLElement | null>(null)
const jsEditorContainer = ref<HTMLElement | null>(null)
let htmlEditor: monaco.editor.IStandaloneCodeEditor | null = null
let cssEditor: monaco.editor.IStandaloneCodeEditor | null = null
let jsEditor: monaco.editor.IStandaloneCodeEditor | null = null

const defaultOptions = {
html: `<div id="diy-container">
<!-- Write your HTML code here -->
</div>`,
css: `/* Write your CSS code here */
#diy-container {
width: 100%;
height: 100%;
}`,
js: `// Write your JavaScript code here
document.addEventListener('DOMContentLoaded', () => {
// Your code here
});`,
}

const compiledCode = computed(() => {
const html = widget.value.options.html || defaultOptions.html
const css = widget.value.options.css || defaultOptions.css
const js = widget.value.options.js || defaultOptions.js

return `${html}
<style>
${css}
</style>
<script>
${js}
<\/script>`
})

const createEditor = (container: HTMLElement, language: string, value: string): monaco.editor.IStandaloneCodeEditor => {
return monaco.editor.create(container, {
value,
language,
theme: 'vs-dark',
minimap: { enabled: false },
fontSize: 14,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
wordWrap: 'on',
})
}

const initEditor = async (): Promise<void> => {
if (htmlEditor || !htmlEditorContainer.value) return
if (cssEditor || !cssEditorContainer.value) return
if (jsEditor || !jsEditorContainer.value) return

htmlEditor = createEditor(htmlEditorContainer.value, 'html', widget.value.options.html || defaultOptions.html)
cssEditor = createEditor(cssEditorContainer.value, 'css', widget.value.options.css || defaultOptions.css)
jsEditor = createEditor(jsEditorContainer.value, 'javascript', widget.value.options.js || defaultOptions.js)
}

const handleDialogOpen = async (): Promise<void> => {
await initEditor()
}

const handleDialogClose = async (): Promise<void> => {
finishEditor()
}

const applyChanges = (): void => {
if (!htmlEditor || !cssEditor || !jsEditor) return
widget.value.options.html = htmlEditor.getValue()
widget.value.options.css = cssEditor.getValue()
widget.value.options.js = jsEditor.getValue()
executeUserScript()
}

const executeUserScript = (): void => {
const js = widget.value.options.js || ''
const scriptElementId = `diy-script-${widget.value.hash}`

// Remove existing script element
document.getElementById(scriptElementId)?.remove()

// Create new script element
const scriptEl = document.createElement('script')
scriptEl.type = 'text/javascript'
scriptEl.textContent = js
scriptEl.id = scriptElementId
document.body.appendChild(scriptEl)
}

const resetChanges = (): void => {
if (!htmlEditor || !cssEditor || !jsEditor) return
htmlEditor.setValue(widget.value.options.html || defaultOptions.html)
cssEditor.setValue(widget.value.options.css || defaultOptions.css)
jsEditor.setValue(widget.value.options.js || defaultOptions.js)
}

const finishEditor = (): void => {
if (htmlEditor) {
htmlEditor.dispose()
htmlEditor = null
}
if (cssEditor) {
cssEditor.dispose()
cssEditor = null
}
if (jsEditor) {
jsEditor.dispose()
jsEditor = null
}
}

const closeDialog = (): void => {
widgetStore.widgetManagerVars(widget.value.hash).configMenuOpen = false
finishEditor()
}

onBeforeMount(() => {
widget.value.options = Object.assign({}, defaultOptions, widget.value.options)
})

onMounted(() => {
executeUserScript()
})

onBeforeUnmount(() => {
finishEditor()
})
</script>

<style scoped>
.main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
min-width: 150px;
min-height: 200px;
}

.edit-button {
position: absolute;
top: 4px;
right: 4px;
z-index: 1;
}

.editor-container {
width: 100%;
height: 24vh;
border: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 1rem;
}

.editor-container:last-child {
margin-bottom: 0;
}
</style>
1 change: 1 addition & 0 deletions src/types/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export enum WidgetType {
CompassHUD = 'CompassHUD',
CustomWidgetBase = 'CustomWidgetBase',
DepthHUD = 'DepthHUD',
DoItYourself = 'DoItYourself',
IFrame = 'IFrame',
ImageView = 'ImageView',
Map = 'Map',
Expand Down

0 comments on commit 063fe88

Please sign in to comment.