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 @@ - + +#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 @@ + + + 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"