diff --git a/backend/src/http.js b/backend/src/http.js
index 90dfaef..d82245d 100644
--- a/backend/src/http.js
+++ b/backend/src/http.js
@@ -14,14 +14,14 @@ export default (sessions) => {
cors(app)
}
- app.get('/containers', async (req, res) => {
+ app.get('/api/containers', async (req, res) => {
return res.json(
(await runningContainers())
.map(c => ({ id: c.Id, image: c.Image, labels: c.Labels }))
)
})
- app.post('/containers', async (req, res) => {
+ app.post('/api/containers', async (req, res) => {
if ((req.body.image && typeof req.body.image !== 'string') || (req.body.cmd && Array.isArray(req.body.cmd))) {
return res.send('invalid arguments').status(401)
}
@@ -31,7 +31,7 @@ export default (sessions) => {
res.send(container)
})
- app.post('/containers/:container/:session/resize', (req, res) => {
+ app.post('/api/containers/:container/sessions/:session/resize', (req, res) => {
const session = sessions[req.params.session]
if (!session || session.container !== req.params.container) {
diff --git a/frontend/package.json b/frontend/package.json
index 5df4fe7..9f2e092 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,7 @@
"dependencies": {
"core-js": "^3.6.5",
"vue": "^3.0.0",
+ "vue-router": "^4.0.0-0",
"xterm": "^4.14.1",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.5.0"
@@ -17,6 +18,7 @@
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
+ "@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-standard": "^5.1.2",
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 7e8ca1e..0e5e4be 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,14 +1,29 @@
-
+
+ Home |
+
+
-
+
+#nav {
+ padding: 30px;
+}
+
+#nav a {
+ font-weight: bold;
+ color: #2c3e50;
+}
+
+#nav a.router-link-exact-active {
+ color: #42b983;
+}
+
diff --git a/frontend/src/components/ContainerList.vue b/frontend/src/components/ContainerList.vue
new file mode 100644
index 0000000..aefbe50
--- /dev/null
+++ b/frontend/src/components/ContainerList.vue
@@ -0,0 +1,23 @@
+
+
+
Containers
+
+ -
+ {{ container.id.substr(0, 8) }}
+
+
+ Start session
+
+
+
+
+
+
+
diff --git a/frontend/src/components/WsTerm.vue b/frontend/src/components/WsTerm.vue
index 3d5fa9a..22915fc 100644
--- a/frontend/src/components/WsTerm.vue
+++ b/frontend/src/components/WsTerm.vue
@@ -8,6 +8,7 @@ import { Terminal } from 'xterm'
import { AttachAddon } from 'xterm-addon-attach'
import { onMounted, ref } from 'vue'
import { FitAddon } from 'xterm-addon-fit'
+import { resizeTerminal } from '../lib/api.js'
export default {
name: 'WsTerm',
@@ -31,14 +32,7 @@ export default {
const resize = () => {
fitAddon.fit()
- fetch(`http://localhost:1234/containers/${props.container}/${props.session}/resize`, {
- method: 'post',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({
- cols: term.cols,
- rows: term.rows
- })
- })
+ resizeTerminal(props.container, props.session, term.cols, term.rows)
}
onMounted(() => {
diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js
new file mode 100644
index 0000000..4879889
--- /dev/null
+++ b/frontend/src/lib/api.js
@@ -0,0 +1,23 @@
+const API_URL = 'http://localhost:1234'
+
+export async function getJson (url) {
+ return (await fetch(url)).json()
+}
+
+export function postJson (url, data) {
+ return fetch(url, {
+ method: 'post',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(data)
+ })
+}
+
+export function getContainers () {
+ return getJson(`${API_URL}/api/containers`)
+}
+
+export function resizeTerminal (containerId, sessionId, cols, rows) {
+ return postJson(`${API_URL}/api/containers/${containerId}/sessions/${sessionId}/resize`, {
+ cols, rows
+ })
+}
diff --git a/frontend/src/lib/helpers.js b/frontend/src/lib/helpers.js
new file mode 100644
index 0000000..c2457e0
--- /dev/null
+++ b/frontend/src/lib/helpers.js
@@ -0,0 +1,8 @@
+const randomChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+
+export function randomString (length = 20) {
+ const values = new Uint32Array(length)
+ window.crypto.getRandomValues(values)
+
+ return Array.from(values.map(v => v % randomChars.length)).map(c => randomChars[c]).join('')
+}
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 01433bc..61a62d7 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -1,4 +1,7 @@
import { createApp } from 'vue'
import App from './App.vue'
+import router from './router'
-createApp(App).mount('#app')
+createApp(App)
+ .use(router)
+ .mount('#app')
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
new file mode 100644
index 0000000..c2f9638
--- /dev/null
+++ b/frontend/src/router/index.js
@@ -0,0 +1,38 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import Home from '../views/Dashboard.vue'
+import { randomString } from '../lib/helpers'
+
+const routes = [
+ {
+ path: '/',
+ name: 'home',
+ component: Home
+ },
+ {
+ path: '/container/:container/session',
+ name: 'new-session',
+ redirect: to => {
+ const container = to.params.container
+ let session = localStorage.getItem(container)
+
+ if (!session) {
+ session = randomString()
+ localStorage.setItem(container, session)
+ }
+
+ return { name: 'session', params: { container, session } }
+ }
+ },
+ {
+ path: '/container/:container/session/:session',
+ name: 'session',
+ component: () => import('../views/Session.vue')
+ }
+]
+
+const router = createRouter({
+ history: createWebHistory(process.env.BASE_URL),
+ routes
+})
+
+export default router
diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue
new file mode 100644
index 0000000..585054d
--- /dev/null
+++ b/frontend/src/views/Dashboard.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/views/Session.vue b/frontend/src/views/Session.vue
new file mode 100644
index 0000000..4f0f0af
--- /dev/null
+++ b/frontend/src/views/Session.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 632e598..85a3eea 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1296,7 +1296,7 @@
webpack "^4.0.0"
yorkie "^2.0.0"
-"@vue/cli-plugin-router@^4.5.15":
+"@vue/cli-plugin-router@^4.5.15", "@vue/cli-plugin-router@~4.5.0":
version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-router/-/cli-plugin-router-4.5.15.tgz#1e75c8c89df42c694f143b9f1028de3cf5d61e1e"
integrity sha512-q7Y6kP9b3k55Ca2j59xJ7XPA6x+iSRB+N4ac0ZbcL1TbInVQ4j5wCzyE+uqid40hLy4fUdlpl4X9fHJEwuVxPA==
@@ -1447,6 +1447,11 @@
optionalDependencies:
prettier "^1.18.2 || ^2.0.0"
+"@vue/devtools-api@^6.0.0-beta.18":
+ version "6.0.0-beta.19"
+ resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.19.tgz#f8e88059daa424515992426a0c7ea5cde07e99bf"
+ integrity sha512-ObzQhgkoVeoyKv+e8+tB/jQBL2smtk/NmC9OmFK8UqdDpoOdv/Kf9pyDWL+IFyM7qLD2C75rszJujvGSPSpGlw==
+
"@vue/eslint-config-standard@^5.1.2":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@vue/eslint-config-standard/-/eslint-config-standard-5.1.2.tgz#c5d55af894a3ae23b65b1af4a425777ac0170b42"
@@ -8546,6 +8551,13 @@ vue-loader@^15.9.2:
vue-hot-reload-api "^2.3.0"
vue-style-loader "^4.1.0"
+vue-router@^4.0.0-0:
+ version "4.0.12"
+ resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.12.tgz#8dc792cddf5bb1abcc3908f9064136de7e13c460"
+ integrity sha512-CPXvfqe+mZLB1kBWssssTiWg4EQERyqJZes7USiqfW9B5N2x+nHlnsM1D3b5CaJ6qgCvMmYJnz+G0iWjNCvXrg==
+ dependencies:
+ "@vue/devtools-api" "^6.0.0-beta.18"
+
vue-style-loader@^4.1.0, vue-style-loader@^4.1.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"