mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Merge pull request #17 from lancatlin/app-graph-frontend
* display graph from data * show the discovery state of each node * accept graph event from socket * store graph in sessionManager * update new graph by file-graph algorithm file-graph: * add file discovery state merging server * update file structure in docker
This commit is contained in:
27 files changed
+426
-227
No files matched your search
@@ -1,12 +1,32 @@
|
||||
name: CI/CD Workflow
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
build-and-push:
|
||||
needs: ci
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
name: ESLint Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
+8
-4
@@ -1,14 +1,18 @@
|
||||
# Build stage
|
||||
FROM node as build-stage
|
||||
WORKDIR /app
|
||||
COPY app/package.json yarn.lock ./
|
||||
RUN yarn install
|
||||
COPY app .
|
||||
COPY package.json yarn.lock ./
|
||||
COPY app/package.json ./app/package.json
|
||||
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY app ./app
|
||||
COPY packages/file-graph ./packages/file-graph
|
||||
RUN yarn build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
COPY --from=build-stage /app/app/dist /usr/share/nginx/html
|
||||
COPY deployment/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
FROM node
|
||||
|
||||
WORKDIR /app
|
||||
COPY app/package.json yarn.lock ./
|
||||
COPY package.json yarn.lock ./
|
||||
COPY app/package.json ./app/package.json
|
||||
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY app .
|
||||
|
||||
WORKDIR /app/app
|
||||
CMD [ "yarn", "dev:docker", "--host", "0.0.0.0" ]
|
||||
@@ -11,6 +11,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@linux-odyssey/file-graph": "*",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
|
||||
@@ -20,6 +20,9 @@ onMounted(async () => {
|
||||
console.log('Disconnected from the server.')
|
||||
})
|
||||
socket.on('message', console.log)
|
||||
socket.on('graph', (event) => {
|
||||
sessionManager.handleGraphUpdate(event)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
|
||||
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
|
||||
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
|
||||
<FileNode :node="child" :pwd="pwd" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
pwd: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
function nodeClass() {
|
||||
const classes = []
|
||||
if (props.node.discovered) classes.push('discovered')
|
||||
if (props.node.path === props.pwd) classes.push('pwd')
|
||||
return classes
|
||||
}
|
||||
</script>
|
||||
@@ -1,56 +1,16 @@
|
||||
<script setup></script>
|
||||
<script setup>
|
||||
import FileNode from './FileNode.vue'
|
||||
import sessionManager from '../utils/session.js'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tree w-max h-max">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#">Parent</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#">Child</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#">Grand Child</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">Child</a>
|
||||
<ul>
|
||||
<li><a href="#">Grand Child</a></li>
|
||||
<li>
|
||||
<a href="#">Grand Child</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#">Great Grand Child</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">Great Grand Child</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">Great Grand Child</a>
|
||||
<ul>
|
||||
<li><a>Grand Child</a></li>
|
||||
<li>
|
||||
<a>Grand Child</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a>Great Grand Child</a>
|
||||
</li>
|
||||
<li>
|
||||
<a>Great Grand Child</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a>Grand Child</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#">Grand Child</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<FileNode
|
||||
:node="sessionManager.graph.value"
|
||||
:pwd="sessionManager.pwd.value"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -149,17 +109,18 @@
|
||||
-moz-transition: all 0.5s;
|
||||
}
|
||||
|
||||
.tree li a:hover,
|
||||
.tree li a:hover + ul li a {
|
||||
a.discovered {
|
||||
background: #c8e4f8;
|
||||
color: #000;
|
||||
border: 1px solid #94a0b4;
|
||||
}
|
||||
|
||||
.tree li a:hover + ul li::after,
|
||||
.tree li a:hover + ul li::before,
|
||||
.tree li a:hover + ul::before,
|
||||
.tree li a:hover + ul ul::before {
|
||||
ul.discovered,
|
||||
li.discovered {
|
||||
border-color: #94a0b4;
|
||||
}
|
||||
|
||||
a.pwd {
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); /* 水平偏移、垂直偏移、模糊半徑、顏色 */
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,19 @@
|
||||
import { ref } from 'vue'
|
||||
import { FileGraph } from '@linux-odyssey/file-graph'
|
||||
import api from './api'
|
||||
|
||||
class SessionManager {
|
||||
constructor(questId) {
|
||||
this.questId = questId
|
||||
this.session = ref(null)
|
||||
this.graph = ref(
|
||||
new FileGraph({
|
||||
path: '/',
|
||||
type: 'folder',
|
||||
discovered: false,
|
||||
})
|
||||
)
|
||||
this.pwd = ref('')
|
||||
}
|
||||
|
||||
getSession() {
|
||||
@@ -14,6 +23,16 @@ class SessionManager {
|
||||
setSession(session) {
|
||||
console.log('Update session:', session)
|
||||
this.session.value = session
|
||||
this.graph.value = new FileGraph(session.graph)
|
||||
}
|
||||
|
||||
handleGraphUpdate(event) {
|
||||
if (event.discover) {
|
||||
this.graph.value.discover(event.discover)
|
||||
}
|
||||
if (event.pwd) {
|
||||
this.pwd.value = event.pwd
|
||||
}
|
||||
}
|
||||
|
||||
async createSession() {
|
||||
@@ -37,7 +56,8 @@ class SessionManager {
|
||||
if (sessions.length === 0) {
|
||||
return
|
||||
}
|
||||
const session = sessions[sessions.length - 1]
|
||||
const { _id: sessionId } = sessions[sessions.length - 1]
|
||||
const { data: session } = await api.get(`/sessions/${sessionId}`)
|
||||
console.log(
|
||||
`Last Session ID: ${session._id}, Quest: ${session.quest}, Created At: ${session.createdAt}`
|
||||
)
|
||||
@@ -49,6 +69,7 @@ class SessionManager {
|
||||
if (!this.session.value) {
|
||||
await this.createSession()
|
||||
}
|
||||
console.log(this.session.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -9,7 +9,8 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
volumes:
|
||||
- ./app/src:/app/src
|
||||
- ./app:/app/app
|
||||
- ./packages/file-graph:/app/packages/file-graph
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
@@ -21,8 +22,9 @@ services:
|
||||
- '3000:3000'
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./server/src:/app/src
|
||||
- ./server/swagger.yaml:/app/swagger.yaml
|
||||
- ./server/src:/app/server/src
|
||||
- ./server/swagger.yaml:/app/server/swagger.yaml
|
||||
- ./packages:/app/packages
|
||||
- ./quests:/app/quests
|
||||
environment:
|
||||
- MONGO_URL=mongodb://db:27017/odyssey-test
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
"scripts": {
|
||||
"app": "yarn workspace app dev",
|
||||
"app:local": "yarn workspace app dev:local",
|
||||
"build": "yarn workspace app build",
|
||||
"server": "yarn workspace server dev",
|
||||
"cli": "yarn workspace client start",
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"format": "prettier --ignore-path .prettierignore --write app server",
|
||||
"test": "yarn node --experimental-vm-modules $(yarn bin jest)",
|
||||
"clean": "yarn workspace server clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { basename } from './utils.js'
|
||||
|
||||
export default class File {
|
||||
constructor({ path, type, discovered }) {
|
||||
this.path = path
|
||||
this.path = path.trimEnd('/')
|
||||
this.name = basename(path)
|
||||
this.type = type
|
||||
this.discovered = discovered
|
||||
@@ -13,6 +13,9 @@ export default class File {
|
||||
}
|
||||
|
||||
contains(file) {
|
||||
return this.isDirectory() && file.path.startsWith(this.path)
|
||||
if (!this.isDirectory()) return false
|
||||
if (this.path === file.path) return true
|
||||
const parentPath = this.path.endsWith('/') ? this.path : `${this.path}/`
|
||||
return file.path.startsWith(parentPath)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import File from './file.js'
|
||||
|
||||
describe('File', () => {
|
||||
let file
|
||||
beforeEach(() => {
|
||||
file = new File({ path: '/home/user', type: 'folder', discovered: true })
|
||||
})
|
||||
|
||||
const filesToCompare = [
|
||||
{
|
||||
name: 'direct children',
|
||||
path: '/home/user/file1.txt',
|
||||
answer: true,
|
||||
},
|
||||
{
|
||||
name: 'nested children',
|
||||
path: '/home/user/folder1/file1.txt',
|
||||
answer: true,
|
||||
},
|
||||
{
|
||||
name: 'outer file',
|
||||
path: '/etc/file1.txt',
|
||||
answer: false,
|
||||
},
|
||||
{
|
||||
name: 'fake children',
|
||||
path: '/home/user2/file2.txt',
|
||||
answer: false,
|
||||
},
|
||||
{
|
||||
name: 'same',
|
||||
path: '/home/user',
|
||||
answer: true,
|
||||
},
|
||||
{
|
||||
name: 'same with trailing',
|
||||
path: '/home/user/',
|
||||
answer: true,
|
||||
},
|
||||
]
|
||||
|
||||
filesToCompare.forEach(({ name, path, answer }) => {
|
||||
test(`${name}: ${path}`, () => {
|
||||
const fileToCompare = new File({ path, type: 'file', discovered: true })
|
||||
console.log(fileToCompare)
|
||||
expect(file.contains(fileToCompare)).toBe(answer)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,17 +1,13 @@
|
||||
import FileNode from './fileNode.js'
|
||||
|
||||
export default class FileGraph {
|
||||
constructor(root) {
|
||||
this.root = new FileNode(root)
|
||||
}
|
||||
|
||||
export default class FileGraph extends FileNode {
|
||||
add(files) {
|
||||
// Logic to add the file node to the graph
|
||||
// You can implement the logic to find the appropriate location in the file graph
|
||||
// If the file node already exists, throw an error
|
||||
|
||||
files.forEach((file) => {
|
||||
this.root.addChild(file, { makeParents: true })
|
||||
this.addChild(file, { makeParents: true })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,7 +17,7 @@ export default class FileGraph {
|
||||
// If the file node doesn't exist, throw an error
|
||||
|
||||
files.forEach((file) => {
|
||||
this.root.removeChild(file)
|
||||
this.removeChild(file)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,7 +32,7 @@ export default class FileGraph {
|
||||
newNode.addChild(file, { makeParents: true })
|
||||
})
|
||||
|
||||
this.root.merge(newNode)
|
||||
this.merge(newNode)
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
|
||||
@@ -30,9 +30,9 @@ describe('FileGraph', () => {
|
||||
|
||||
fileGraph.add(filesToAdd)
|
||||
|
||||
console.log(fileGraph.root.toString())
|
||||
expect(fileGraph.root.children.length).toBe(1)
|
||||
expect(fileGraph.root.children[0].children[0].children.length).toBe(2)
|
||||
console.log(fileGraph.toString())
|
||||
expect(fileGraph.children.length).toBe(1)
|
||||
expect(fileGraph.children[0].children[0].children.length).toBe(2)
|
||||
})
|
||||
|
||||
test('remove files from graph', () => {
|
||||
@@ -76,14 +76,44 @@ describe('FileGraph', () => {
|
||||
|
||||
fileGraph.remove(filesToRemove)
|
||||
|
||||
console.log(fileGraph.root.toString())
|
||||
expect(fileGraph.root.children[0].children[0].children.length).toBe(1)
|
||||
expect(fileGraph.root.children[0].children[0].children[0].name).toBe(
|
||||
'folder1'
|
||||
console.log(fileGraph.toString())
|
||||
expect(fileGraph.children[0].children[0].children.length).toBe(1)
|
||||
expect(fileGraph.children[0].children[0].children[0].name).toBe('folder1')
|
||||
expect(fileGraph.children[0].children[0].children[0].children.length).toBe(
|
||||
0
|
||||
)
|
||||
})
|
||||
|
||||
test('discover', () => {
|
||||
const fileGraph = new FileGraph(root)
|
||||
const discoverFiles = [
|
||||
{
|
||||
path: '/home/user/folder1',
|
||||
type: 'folder',
|
||||
discovered: true,
|
||||
},
|
||||
{
|
||||
path: '/home/user/folder1/file1.txt',
|
||||
type: 'file',
|
||||
discovered: true,
|
||||
},
|
||||
{
|
||||
path: '/home/user/folder1/file2.txt',
|
||||
type: 'file',
|
||||
discovered: true,
|
||||
},
|
||||
{
|
||||
path: '/home/user/folder1/file3.txt',
|
||||
type: 'file',
|
||||
discovered: true,
|
||||
},
|
||||
]
|
||||
|
||||
fileGraph.discover(discoverFiles)
|
||||
console.log(fileGraph.toString())
|
||||
expect(fileGraph.children[0].children[0].children[0].children.length).toBe(
|
||||
3
|
||||
)
|
||||
expect(
|
||||
fileGraph.root.children[0].children[0].children[0].children.length
|
||||
).toBe(0)
|
||||
})
|
||||
|
||||
test('discover new files', () => {
|
||||
@@ -113,7 +143,7 @@ describe('FileGraph', () => {
|
||||
]
|
||||
|
||||
fileGraph.add(filesToAdd)
|
||||
console.log(fileGraph.root.toString())
|
||||
console.log(fileGraph.toString())
|
||||
|
||||
const newFiles = [
|
||||
{
|
||||
@@ -140,10 +170,10 @@ describe('FileGraph', () => {
|
||||
|
||||
fileGraph.discover(newFiles)
|
||||
|
||||
console.log(fileGraph.root.toString())
|
||||
expect(fileGraph.root.children[0].children[0].children.length).toBe(2)
|
||||
expect(
|
||||
fileGraph.root.children[0].children[0].children[0].children.length
|
||||
).toBe(3)
|
||||
console.log(fileGraph.toString())
|
||||
expect(fileGraph.children[0].children[0].children.length).toBe(2)
|
||||
expect(fileGraph.children[0].children[0].children[0].children.length).toBe(
|
||||
3
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,8 @@ import { basename, dirname } from './utils.js'
|
||||
export default class FileNode extends File {
|
||||
constructor(file) {
|
||||
super(file)
|
||||
this.children = file.children || []
|
||||
this.children = []
|
||||
if (file.children) this.children = file.children.map((f) => new FileNode(f))
|
||||
}
|
||||
|
||||
addChild(fileInput, { makeParents } = {}) {
|
||||
@@ -76,7 +77,12 @@ export default class FileNode extends File {
|
||||
// Merge the children of the provided file node into this node
|
||||
// If a child already exists, merge the children of that child
|
||||
// If a child does not exist, add it to this node
|
||||
|
||||
// If this is the same node, merge the children
|
||||
if (fileNode.path === this.path) {
|
||||
// Update the state of this node
|
||||
this.discovered = this.discovered || fileNode.discovered
|
||||
|
||||
fileNode.children.forEach((child) => {
|
||||
const existingChild = this.children.find((c) => c.path === child.path)
|
||||
if (existingChild) {
|
||||
@@ -92,12 +98,17 @@ export default class FileNode extends File {
|
||||
fileNode.children.some((c) => c.path === child.path)
|
||||
)
|
||||
}
|
||||
} else if (this.contains(fileNode)) {
|
||||
}
|
||||
// If this contains the parent node
|
||||
else if (this.contains(fileNode)) {
|
||||
const childNode = this.children.find((child) => child.contains(fileNode))
|
||||
// If a child node exists, merge the provided node into the child node
|
||||
if (childNode) {
|
||||
childNode.merge(fileNode)
|
||||
} else {
|
||||
this.addChild(fileNode)
|
||||
}
|
||||
// If a child node does not exist, add the provided node as a child
|
||||
else {
|
||||
this.addChild(fileNode, { makeParents: true })
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -108,12 +119,15 @@ export default class FileNode extends File {
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
const indent = ' '.repeat(this.path.split('/').length - 1)
|
||||
const suffix = this.isDirectory() ? '/' : ''
|
||||
toString(level = 0) {
|
||||
let { name } = this
|
||||
const indent = ' '.repeat(level)
|
||||
name = `${indent}${name}`
|
||||
if (this.isDirectory()) name += '/'
|
||||
if (!this.discovered) name += `*`
|
||||
const childrenString = this.children
|
||||
.map((child) => child.toString())
|
||||
.map((child) => child.toString(level + 1))
|
||||
.join('')
|
||||
return `${indent}${this.name}${suffix}\n${childrenString}`
|
||||
return `${name}\n${childrenString}`
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,35 @@ describe('FileNode', () => {
|
||||
mergeTest(files1, files2, expected)
|
||||
})
|
||||
|
||||
test('merge-hidden', () => {
|
||||
const root = {
|
||||
path: '/',
|
||||
type: 'folder',
|
||||
}
|
||||
const files1 = [
|
||||
root,
|
||||
{ path: '/a', type: 'file', discovered: true },
|
||||
{ path: '/.b', type: 'file', discovered: false },
|
||||
{ path: '/c', type: 'folder', discovered: true },
|
||||
{ path: '/c/.d', type: 'file', discovered: false },
|
||||
]
|
||||
|
||||
const files2 = [
|
||||
{ path: '/c', type: 'folder', discovered: true },
|
||||
{ path: '/c/.d', type: 'file', discovered: true },
|
||||
]
|
||||
|
||||
const expected = [
|
||||
root,
|
||||
{ path: '/a', type: 'file', discovered: true },
|
||||
{ path: '/.b', type: 'file', discovered: false },
|
||||
{ path: '/c', type: 'folder', discovered: true },
|
||||
{ path: '/c/.d', type: 'file', discovered: true },
|
||||
]
|
||||
|
||||
mergeTest(files1, files2, expected)
|
||||
})
|
||||
|
||||
test('remove-child', () => {
|
||||
const root = {
|
||||
path: '/',
|
||||
@@ -219,3 +248,39 @@ describe('FileNode', () => {
|
||||
removeChildTest(files, fileToRemove, expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('load nested fileNode', () => {
|
||||
const data = {
|
||||
path: '/',
|
||||
type: 'folder',
|
||||
children: [
|
||||
{
|
||||
path: '/a',
|
||||
type: 'file',
|
||||
},
|
||||
{
|
||||
path: '/b',
|
||||
type: 'folder',
|
||||
children: [
|
||||
{
|
||||
path: '/b/c',
|
||||
type: 'folder',
|
||||
children: [
|
||||
{
|
||||
path: '/b/c/d',
|
||||
type: 'file',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const node = new FileNode(data)
|
||||
|
||||
console.log(node.toString())
|
||||
expect(node.children.length).toBe(2)
|
||||
expect(node.children[1].children.length).toBe(1)
|
||||
expect(node.children[1].children[0].children.length).toBe(1)
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as File } from './file.js'
|
||||
export { default as FileNode } from './fileNode.js'
|
||||
export { default as FileGraph } from './fileGraph.js'
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "file-graph",
|
||||
"name": "@linux-odyssey/file-graph",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"exports": "./index.js",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "yarn node --experimental-vm-modules $(yarn bin jest)"
|
||||
}
|
||||
"scripts": {}
|
||||
}
|
||||
+7
-3
@@ -1,13 +1,17 @@
|
||||
FROM node
|
||||
|
||||
WORKDIR /app
|
||||
COPY server/package.json yarn.lock ./
|
||||
COPY ./package.json yarn.lock ./
|
||||
COPY server/package.json ./server/package.json
|
||||
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||
RUN yarn install --frozen-lockfile --production
|
||||
|
||||
COPY server/src ./src
|
||||
COPY server/swagger.yaml .
|
||||
COPY ./server ./server
|
||||
COPY packages/file-graph ./packages/file-graph
|
||||
COPY quests ./quests
|
||||
|
||||
WORKDIR /app/server
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
FROM node
|
||||
|
||||
WORKDIR /app
|
||||
COPY server/package.json yarn.lock ./
|
||||
COPY package.json yarn.lock ./
|
||||
COPY server/package.json ./server/package.json
|
||||
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
ENV NODE_ENV=development
|
||||
@@ -9,4 +11,4 @@ ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "yarn", "dev" ]
|
||||
CMD [ "yarn", "server" ]
|
||||
@@ -10,6 +10,7 @@
|
||||
"clean": "node src/cli/clean.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@linux-odyssey/file-graph": "*",
|
||||
"commander": "^11.0.0",
|
||||
"dockerode": "^3.3.5",
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Command from '../../models/command.js'
|
||||
import Session from '../../models/session.js'
|
||||
import { verifySessionJWT } from '../../utils/auth.js'
|
||||
import commandHandler from '../../game/commandHandler.js'
|
||||
import CommandHandler from '../../game/commandHandler.js'
|
||||
|
||||
export async function newCommand(req, res) {
|
||||
console.log('new command:', req.body)
|
||||
@@ -48,7 +48,9 @@ export async function newCommand(req, res) {
|
||||
|
||||
await c.save()
|
||||
|
||||
const response = await commandHandler(session, c, additionalData)
|
||||
const commandHandler = new CommandHandler(session, c, additionalData)
|
||||
|
||||
const response = await commandHandler.run()
|
||||
console.log(response)
|
||||
|
||||
res.status(201).json(response)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import minimist from 'minimist'
|
||||
import { FileGraph } from '@linux-odyssey/file-graph'
|
||||
import Quest from '../models/quest.js'
|
||||
import { pushToSession } from '../api/socket.js'
|
||||
|
||||
@@ -11,48 +12,89 @@ const checkMatch = (pattern, input) => {
|
||||
})
|
||||
}
|
||||
|
||||
async function commandHandler(session, commandInput, additionalData) {
|
||||
const quest = await Quest.findById(session.quest)
|
||||
const stage = quest.stages.find((s) => s.id === session.progress)
|
||||
if (!stage) {
|
||||
console.error('stage not found', session.progress)
|
||||
return {}
|
||||
export default class CommandHandler {
|
||||
constructor(session, commandInput, additionalData) {
|
||||
this.session = session
|
||||
this.commandInput = commandInput
|
||||
this.argv = minimist(this.commandInput.command.split(' '))
|
||||
|
||||
this.additionalData = additionalData
|
||||
}
|
||||
|
||||
const argv = minimist(commandInput.command.split(' '))
|
||||
|
||||
if ('discover' in additionalData) {
|
||||
pushToSession(session.id, 'graph', {
|
||||
pwd: commandInput.pwd,
|
||||
discover: additionalData.discover,
|
||||
})
|
||||
} else if (argv._[0] === 'cd') {
|
||||
pushToSession(session.id, 'graph', { pwd: commandInput.pwd })
|
||||
handleEvent() {
|
||||
if (this.additionalData.discover) this.discoverHandler()
|
||||
}
|
||||
|
||||
const commandMatch = checkMatch(stage.condition.command, commandInput.command)
|
||||
const outputMatch = checkMatch(stage.condition.output, commandInput.output)
|
||||
const errorMatch = checkMatch(stage.condition.error, commandInput.output)
|
||||
handleCommand() {
|
||||
const command = this.argv._[0]
|
||||
switch (command) {
|
||||
case 'cd':
|
||||
pushToSession(this.session.id, 'graph', {
|
||||
pwd: this.commandInput.pwd,
|
||||
})
|
||||
break
|
||||
|
||||
if (!commandMatch || !outputMatch || !errorMatch) return {}
|
||||
case 'ls':
|
||||
pushToSession(this.session.id, 'graph', {
|
||||
pwd: this.commandInput.pwd,
|
||||
discover: this.additionalData.discover,
|
||||
})
|
||||
break
|
||||
|
||||
session.completion.push(session.progress)
|
||||
session.hints.push(...stage.hints)
|
||||
|
||||
session.progress = stage.next
|
||||
await session.save()
|
||||
|
||||
if (stage.next === 'END') {
|
||||
session.status = 'finished'
|
||||
session.finishedAt = new Date()
|
||||
await session.save()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
responses: stage.responses,
|
||||
hints: stage.hints,
|
||||
end: stage.next === 'END',
|
||||
async discoverHandler() {
|
||||
const graph = new FileGraph(this.session.graph)
|
||||
graph.discover(this.additionalData.discover)
|
||||
this.session.graph = graph
|
||||
await this.session.save()
|
||||
}
|
||||
|
||||
async run() {
|
||||
const quest = await Quest.findById(this.session.quest)
|
||||
const stage = quest.stages.find((s) => s.id === this.session.progress)
|
||||
if (!stage) {
|
||||
console.error('stage not found', this.session.progress)
|
||||
return {}
|
||||
}
|
||||
|
||||
this.handleEvent()
|
||||
this.handleCommand()
|
||||
|
||||
const commandMatch = checkMatch(
|
||||
stage.condition.command,
|
||||
this.commandInput.command
|
||||
)
|
||||
const outputMatch = checkMatch(
|
||||
stage.condition.output,
|
||||
this.commandInput.output
|
||||
)
|
||||
const errorMatch = checkMatch(
|
||||
stage.condition.error,
|
||||
this.commandInput.output
|
||||
)
|
||||
|
||||
if (!commandMatch || !outputMatch || !errorMatch) return {}
|
||||
|
||||
this.session.completion.push(this.session.progress)
|
||||
this.session.hints.push(...stage.hints)
|
||||
|
||||
this.session.progress = stage.next
|
||||
await this.session.save()
|
||||
|
||||
if (stage.next === 'END') {
|
||||
this.session.status = 'finished'
|
||||
this.session.finishedAt = new Date()
|
||||
await this.session.save()
|
||||
}
|
||||
|
||||
return {
|
||||
responses: stage.responses,
|
||||
hints: stage.hints,
|
||||
end: stage.next === 'END',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default commandHandler
|
||||
@@ -1,4 +1,18 @@
|
||||
import { model, Schema } from 'mongoose'
|
||||
import { FileNode } from '@linux-odyssey/file-graph'
|
||||
|
||||
const nodeSchema = new Schema({
|
||||
path: String,
|
||||
type: String,
|
||||
discovered: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
nodeSchema.add({
|
||||
children: [nodeSchema],
|
||||
})
|
||||
|
||||
const Session = model(
|
||||
'Session',
|
||||
@@ -28,6 +42,13 @@ const Session = model(
|
||||
progress: String,
|
||||
completion: [String],
|
||||
hints: [String],
|
||||
graph: {
|
||||
type: nodeSchema,
|
||||
default: new FileNode({
|
||||
path: '/',
|
||||
type: 'folder',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import yaml from 'yaml'
|
||||
|
||||
import Quest from '../models/quest.js'
|
||||
|
||||
const questDirectory = path.join(process.cwd(), 'quests')
|
||||
const questDirectory = path.join(process.cwd(), '..', 'quests')
|
||||
|
||||
export default async function loadAndUpdateQuests() {
|
||||
const questNames = await fs.readdir(questDirectory)
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import jest from 'jest'
|
||||
import * as sessionController from '../../src/api/controllers/sessionController'
|
||||
import Session from '../../src/models/session'
|
||||
|
||||
jest.mock('../../src/models/session')
|
||||
|
||||
describe('Session Controller', () => {
|
||||
let mockReq
|
||||
let mockRes
|
||||
|
||||
beforeEach(() => {
|
||||
mockReq = {
|
||||
user: { id: 'userid' },
|
||||
body: {},
|
||||
params: {},
|
||||
}
|
||||
mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
}
|
||||
})
|
||||
|
||||
test('getSessionList returns a list of sessions', async () => {
|
||||
Session.find.mockResolvedValue(['session1', 'session2'])
|
||||
await sessionController.getSessionList(mockReq, mockRes)
|
||||
expect(mockRes.json).toHaveBeenCalledWith(['session1', 'session2'])
|
||||
})
|
||||
|
||||
test('createSession creates a new session', async () => {
|
||||
mockReq.body.exercise_id = 'exerciseid'
|
||||
const mockSession = { user: 'userid', exercise: 'exerciseid' }
|
||||
Session.mockReturnValue(mockSession)
|
||||
mockSession.save = jest.fn().mockResolvedValue('newsess')
|
||||
await sessionController.createSession(mockReq, mockRes)
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201)
|
||||
expect(mockRes.json).toHaveBeenCalledWith('newsess')
|
||||
})
|
||||
|
||||
test('getSessionById returns the correct session', async () => {
|
||||
mockReq.params.session_id = 'sessionid'
|
||||
const mockSession = { user: 'userid', _id: 'sessionid' }
|
||||
Session.findOne.mockResolvedValue(mockSession)
|
||||
await sessionController.getSessionById(mockReq, mockRes)
|
||||
expect(mockRes.json).toHaveBeenCalledWith(mockSession)
|
||||
})
|
||||
|
||||
test('deleteSessionById deletes a session', async () => {
|
||||
mockReq.params.session_id = 'sessionid'
|
||||
const mockSession = { user: 'userid', _id: 'sessionid' }
|
||||
Session.findOneAndDelete.mockResolvedValue(mockSession)
|
||||
await sessionController.deleteSessionById(mockReq, mockRes)
|
||||
expect(mockRes.status).toHaveBeenCalledWith(204)
|
||||
expect(mockRes.end).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user