mirror of
https://github.com/Av0idD4rk/packmate.git
synced 2026-10-23 16:31:01 +00:00
Начал переписывать
This commit is contained in:
parent
b8c21d3a86
commit
e79f5cbce4
116
.gitignore
vendored
116
.gitignore
vendored
@ -1,21 +1,101 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
cmake-build-debug/
|
||||
cmake-build-release/
|
||||
.idea/**/mongoSettings.xml
|
||||
*.iws
|
||||
out/
|
||||
.idea_modules/
|
||||
atlassian-ide-plugin.xml
|
||||
.idea/replstate.xml
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
.idea/httpRequests
|
||||
*.bak
|
||||
/.tgitconfig
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
lib-cov
|
||||
coverage
|
||||
.nyc_output
|
||||
.grunt
|
||||
bower_components
|
||||
.lock-wscript
|
||||
build/Release
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
typings/
|
||||
.npm
|
||||
.eslintcache
|
||||
.node_repl_history
|
||||
*.tgz
|
||||
.yarn-integrity
|
||||
.env
|
||||
.next
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
*.stackdump
|
||||
[Dd]esktop.ini
|
||||
$RECYCLE.BIN/
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
*.lnk
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-v][a-z]
|
||||
[._]sw[a-p]
|
||||
Session.vim
|
||||
.netrwhist
|
||||
*~
|
||||
tags
|
||||
[._]*.un~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
.org-id-locations
|
||||
*_archive
|
||||
*_flymake.*
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
/elpa/
|
||||
*.rel
|
||||
/auto/
|
||||
.cask/
|
||||
dist/
|
||||
flycheck_*.el
|
||||
/server/
|
||||
.projectile
|
||||
.dir-locals.el
|
||||
.fuse_hidden*
|
||||
.directory
|
||||
.Trash-*
|
||||
.nfs*
|
||||
$*$
|
||||
|
||||
69
.idea/codeStyles/Project.xml
Normal file
69
.idea/codeStyles/Project.xml
Normal file
@ -0,0 +1,69 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="LINE_SEPARATOR" value=" " />
|
||||
<option name="FORMATTER_TAGS_ENABLED" value="true" />
|
||||
<option name="SOFT_MARGINS" value="90,120" />
|
||||
<CssCodeStyleSettings>
|
||||
<option name="HEX_COLOR_UPPER_CASE" value="true" />
|
||||
</CssCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<codeStyleSettings language="CSS">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="CoffeeScript">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="LESS">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="SASS">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="SCSS">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Stylus">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
9
.idea/discord.xml
Normal file
9
.idea/discord.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="true" />
|
||||
</component>
|
||||
<component name="ProjectNotificationSettings">
|
||||
<option name="askShowProject" value="false" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/misc.xml
Normal file
6
.idea/misc.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/packmate-frontend.iml" filepath="$PROJECT_DIR$/.idea/packmate-frontend.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/packmate-frontend.iml
Normal file
8
.idea/packmate-frontend.iml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -2,4 +2,7 @@ module.exports = {
|
||||
presets: [
|
||||
'@vue/app',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
],
|
||||
};
|
||||
|
||||
3028
package-lock.json
generated
3028
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@ -8,26 +8,28 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"axios": "^0.19.0",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bootstrap-vue": "^2.0.1",
|
||||
"core-js": "^2.6.5",
|
||||
"jquery": "^3.4.0",
|
||||
"popper.js": "^1.15.0",
|
||||
"sockjs-client": "^1.3.0",
|
||||
"sockjs-client": "^1.4.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-infinite-loading": "^2.4.4",
|
||||
"vue-router": "^3.0.3",
|
||||
"vuex": "^3.0.1",
|
||||
"vuex-persistedstate": "^2.5.4"
|
||||
"vuex-persistedstate": "^2.5.4",
|
||||
"vuex-shared-mutations": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.7.0",
|
||||
"@vue/cli-plugin-eslint": "^3.7.0",
|
||||
"@vue/cli-service": "^3.7.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.6.0",
|
||||
"@fortawesome/fontawesome-free": "^5.11.1",
|
||||
"@vue/cli-plugin-babel": "^3.11.0",
|
||||
"@vue/cli-plugin-eslint": "^3.11.0",
|
||||
"@vue/cli-service": "^3.11.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
@ -40,9 +42,9 @@
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"warn",
|
||||
"tab"
|
||||
"off"
|
||||
],
|
||||
"no-mixed-spaces-and-tabs": "off",
|
||||
"comma-dangle": [
|
||||
"warn",
|
||||
"always"
|
||||
|
||||
@ -1,28 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!--suppress HtmlUnknownTag -->
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= BASE_URL %>apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= BASE_URL %>favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= BASE_URL %>favicon-16x16.png">
|
||||
<link rel="manifest" href="<%= BASE_URL %>site.webmanifest">
|
||||
<link rel="mask-icon" href="<%= BASE_URL %>safari-pinned-tab.svg" color="#44beff">
|
||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono&subset=cyrillic" rel="stylesheet">
|
||||
<meta name="msapplication-TileColor" content="#afd9f0">
|
||||
<meta name="theme-color" content="#afd9f0">
|
||||
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible">
|
||||
<title>Packmate</title>
|
||||
|
||||
<meta name="author" content="saber-nyan & serega6531">
|
||||
|
||||
<meta content="width=device-width,initial-scale=1.0" name="viewport">
|
||||
<link href="<%= BASE_URL %>favicon.ico" rel="icon">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but Packmate doesn't work properly without JavaScript enabled. Please enable it to
|
||||
continue.</strong>
|
||||
<strong>Packmate не будет работать без JS, включи его!</strong>
|
||||
</noscript>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
120
src/App.vue
120
src/App.vue
@ -1,11 +1,127 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
<Navbar></Navbar>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view name="sidebar"></router-view>
|
||||
</transition>
|
||||
<!-- <Sidebar></Sidebar>-->
|
||||
<main role="main" class="col-sm-9 ml-sm-auto px-4">
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view name="content"></router-view>
|
||||
</transition>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<Settings></Settings>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Navbar from './components/Navbar';
|
||||
import Settings from './views/Settings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Settings,
|
||||
Navbar,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFWJ0bbck.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFUZ0bbck.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFWZ0bbck.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFVp0bbck.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFWp0bbck.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFW50bbck.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFVZ0b.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
html * {
|
||||
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: .875rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/*noinspection CssUnusedSymbol*/
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .3s;
|
||||
}
|
||||
|
||||
/*noinspection CssUnusedSymbol*/
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[role="main"] {
|
||||
padding-top: 55px; /* Space for fixed navbar */
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<div class="modal-mask">
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-container">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="inputServiceName">Имя сервиса</label>
|
||||
<input type="text" class="form-control" id="inputServiceName" v-model="service.name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputServicePort">Порт</label>
|
||||
<input type="number" class="form-control" id="inputServicePort" v-model="service.port">
|
||||
</div>
|
||||
<button type="submit" @click.prevent="sendNewService()" class="btn btn-primary" style="margin-right: 10px;">
|
||||
Сохранить
|
||||
</button>
|
||||
<button type="submit" @click.prevent="$emit('close')" class="btn btn-secondary">
|
||||
Закрыть
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
name: "AddServiceModal",
|
||||
data: function () {
|
||||
return {
|
||||
escapeKeyCallback: (e) => {
|
||||
if (e.key === 'Escape') this.$emit('close');
|
||||
},
|
||||
service: {
|
||||
name: '',
|
||||
port: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keyup', this.$data.escapeKeyCallback);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keyup', this.$data.escapeKeyCallback);
|
||||
},
|
||||
methods: {
|
||||
sendNewService() {
|
||||
const instance = axios.create({
|
||||
baseURL: this.$store.state.apiUrl,
|
||||
auth: {
|
||||
username: this.$store.state.apiLogin,
|
||||
password: this.$store.state.apiPassword,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
instance.post('/service/', this.service)
|
||||
.then(response => this.$emit('serviceAddDone', response.data))
|
||||
.catch(error => console.error('Failed to add service', error));
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
z-index: 9998;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
display: table;
|
||||
transition: opacity .3s ease;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 30px;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
|
||||
transition: all .3s ease;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
@ -1,102 +0,0 @@
|
||||
<template>
|
||||
<main class="col-md-9 ml-sm-auto col-lg-10 px-4">
|
||||
<div class="custom-control custom-switch" style="margin-bottom: 10px;">
|
||||
<input type="checkbox" class="custom-control-input" id="favSwitch" v-model="displayFavOnly">
|
||||
<label class="custom-control-label" for="favSwitch">Только избранные</label>
|
||||
</div>
|
||||
<p>
|
||||
<span style="background: rgba(224, 246, 255, 0.4); box-shadow: 0 0 5px 5px rgba(224, 246, 255, 0.4);">Ответ</span>, <span style="background: rgba(251, 233, 231, 0.4); box-shadow: 0 0 5px 5px rgba(251, 233, 231, 0.4);">запрос</span>.
|
||||
</p>
|
||||
<Packet v-for="packet in packets"
|
||||
:key="packet.id"
|
||||
:id="packet.id"
|
||||
:content="packet.content"
|
||||
:timestamp="packet.timestamp"
|
||||
:isIncoming="packet.incoming"></Packet>
|
||||
<infinite-loading @infinite="infiniteHandler" spinner="waveDots" ref="infiniteLoading"></infinite-loading>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Packet from "@/components/Packet.vue";
|
||||
import InfiniteLoading from "vue-infinite-loading";
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
name: 'Content',
|
||||
props: ['servicePort', 'streamId',],
|
||||
data: function () {
|
||||
return {
|
||||
packets: [],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'streamId': function () {
|
||||
this.packets = [];
|
||||
this.$refs.infiniteLoading.stateChanger.reset();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
infiniteHandler($state) {
|
||||
if (!this.streamId) return $state.complete();
|
||||
const instance = axios.create({
|
||||
baseURL: this.$store.state.apiUrl,
|
||||
auth: {
|
||||
username: this.$store.state.apiLogin,
|
||||
password: this.$store.state.apiPassword,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const packets = this.packets;
|
||||
let startsFrom;
|
||||
if (packets && packets.length && packets[packets.length - 1]) {
|
||||
startsFrom = packets[packets.length - 1].id;
|
||||
} else {
|
||||
startsFrom = 0;
|
||||
}
|
||||
|
||||
instance.post(`/packet/${this.streamId}`, {
|
||||
direction: 'ASC',
|
||||
startingFrom: startsFrom,
|
||||
pageSize: parseInt(this.$store.state.pageSize),
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
if (data.length === 0) return $state.complete();
|
||||
if (data[0] && this.packets[0] && data[0].id === this.packets[0].id) {
|
||||
$state.complete();
|
||||
} else {
|
||||
this.packets.push(...data);
|
||||
$state.loaded();
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Failed to load new portion of packets', error);
|
||||
return $state.error();
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
displayFavOnly: {
|
||||
get() {
|
||||
return this.$store.state.displayFavOnly;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setDisplayFavOnly', value);
|
||||
this.$emit('displayFavChanged');
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {Packet, InfiniteLoading,},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
padding-top: 50px; /* Space for fixed navbar */
|
||||
max-width: 80%;
|
||||
margin-left: auto !important;
|
||||
}
|
||||
</style>
|
||||
@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<li class="nav-item" :class="{ 'active': subIsActive('/' + port) }">
|
||||
<router-link class="nav-link" :to="{name: 'home', params: {servicePort: port}}">{{ name }} ({{ port }})
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CtfService",
|
||||
methods: {
|
||||
subIsActive(prefix) {
|
||||
const paths = Array.isArray(prefix) ? prefix : [prefix,];
|
||||
return paths.some(path => {
|
||||
return this.$route.path.indexOf(path) === 0;
|
||||
})
|
||||
},
|
||||
},
|
||||
props: ['port', 'name',],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<div class="dropdown-item" :style="'color:' + color"
|
||||
@click="openPattern">
|
||||
{{name}}: {{regex ? `/${value}/` : `'${value}'`}}
|
||||
<div class="float-right">
|
||||
<button @click.prevent.stop="deleteSelf()" class="btn btn-outline-danger btn-block btn-sm" type="button">x</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import $ from "jquery";
|
||||
|
||||
export default {
|
||||
name: "FlagPattern",
|
||||
props: ['name', 'value', 'color', 'id', 'regex',],
|
||||
methods: {
|
||||
openPattern() {
|
||||
this.$router.push({name: 'home', params: this.$route.params, query: {pId: this.id,},});
|
||||
// $('.dropdown').trigger('hide.bs.dropdown')
|
||||
$('.dropdown-toggle').dropdown('toggle')
|
||||
},
|
||||
deleteSelf() {
|
||||
const instance = axios.create({
|
||||
baseURL: this.$store.state.apiUrl,
|
||||
auth: {
|
||||
username: this.$store.state.apiLogin,
|
||||
password: this.$store.state.apiPassword,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
instance.delete(`/pattern/${this.id}`)
|
||||
.then(() => {
|
||||
this.$destroy();
|
||||
this.$el.parentNode.removeChild(this.$el); // FIXME: может сломать Vue, лучше бы это убрать...
|
||||
})
|
||||
.catch(error => console.error('failed to delete pattern', error));
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
</style>
|
||||
@ -1,179 +1,106 @@
|
||||
<template>
|
||||
<nav class="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow navbar-expand-lg navbar-expand">
|
||||
|
||||
<!-- <router-link to="/" class="navbar-brand col-sm-3 col-md-2 mr-0">Packmate</router-link>-->
|
||||
<div class="dropdown">
|
||||
<form> <!-- Disable autohide -->
|
||||
<button class="btn btn-dark dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
Паттерны
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Name" v-model="newPattern.name">
|
||||
<input type="text" class="form-control" placeholder="Value" v-model="newPattern.value">
|
||||
<input type="text" class="form-control" placeholder="#Color" v-model="newPattern.color">
|
||||
<div class="input-group-append">
|
||||
<button @click.prevent="createPattern()" class="btn btn-outline-primary" type="button">+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-control custom-switch" style="margin-left: 10px; margin-bottom: 8px;">
|
||||
<input type="checkbox" class="custom-control-input"
|
||||
id="isRegexCheckbox" v-model="newPattern.regex">
|
||||
<label class="custom-control-label" for="isRegexCheckbox">Regex?</label>
|
||||
</div>
|
||||
|
||||
<FlagPattern v-for="pattern in patterns"
|
||||
:key="pattern.id"
|
||||
:id="pattern.id"
|
||||
:name="pattern.name"
|
||||
:value="pattern.value"
|
||||
:color="pattern.color"
|
||||
:regex="pattern.regex"></FlagPattern>
|
||||
</div>
|
||||
</form>
|
||||
<nav class="navbar navbar-dark navbar-expand fixed-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
<div class="navbar-brand col-sm-3 mr-0" style="cursor: pointer;">
|
||||
Паттерны
|
||||
<i class="fas fa-angle-down" style="margin-left: 0.5em"></i>
|
||||
</div>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item" :class="{ 'active': !this.$route.params.servicePort }">
|
||||
<router-link class="nav-link" to="/">Все</router-link>
|
||||
</li>
|
||||
<CtfService
|
||||
v-for="service in ctfServices"
|
||||
:key="service.port"
|
||||
:port="service.port"
|
||||
:name="service.name"></CtfService>
|
||||
|
||||
<li class="nav-item" style="margin-left: 10px;">
|
||||
<button type="button" class="btn btn-link" @click="$emit('addServiceButtonClicked')">
|
||||
<svg id="i-plus" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16"
|
||||
fill="none" stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M16 2 L16 30 M2 16 L30 16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="navbar-nav px-3 mr-auto">
|
||||
<li class="nav-item text-nowrap">
|
||||
<router-link class="nav-link" to="/" exact>Все</router-link>
|
||||
</li>
|
||||
<NavbarService v-for="service in services"
|
||||
:key="service.name"
|
||||
:name="service.name"
|
||||
:port="service.port"/>
|
||||
</ul>
|
||||
<div class="my-2 my-lg-0" style="margin-right: 10px;">
|
||||
<button type="button" class="btn btn-link" @click="$emit('settingsButtonClicked')">
|
||||
<svg id="i-settings" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16"
|
||||
fill="none" stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M13 2 L13 6 11 7 8 4 4 8 7 11 6 13 2 13 2 19 6 19 7 21 4 24 8 28 11 25 13 26 13 30 19 30 19 26 21 25 24 28 28 24 25 21 26 19 30 19 30 13 26 13 25 11 28 8 24 4 21 7 19 6 19 2 Z"></path>
|
||||
<circle cx="16" cy="16" r="4"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="my-2 my-lg-0 mr-3 navbar-cogs" style="cursor: pointer;"
|
||||
@click.stop.prevent="showSettings">
|
||||
<i class="fas fa-cogs"></i>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CtfService from "@/components/CtfService.vue";
|
||||
import axios from "axios";
|
||||
import $ from "jquery";
|
||||
import FlagPattern from "@/components/FlagPattern.vue";
|
||||
import NavbarService from './NavbarService';
|
||||
|
||||
export default {
|
||||
name: "Navbar",
|
||||
data: function () {
|
||||
return {
|
||||
ctfServices: [],
|
||||
patterns: [],
|
||||
newPattern: {
|
||||
name: '',
|
||||
value: '',
|
||||
color: '',
|
||||
regex: false,
|
||||
export default {
|
||||
name: 'Navbar',
|
||||
data() {
|
||||
return {
|
||||
services: Array(),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.updateServices();
|
||||
},
|
||||
methods: {
|
||||
showSettings() {
|
||||
this.$bvModal.show('settingsModal');
|
||||
console.debug('Showing settings...');
|
||||
},
|
||||
updateServices() {
|
||||
this.$http.get('service/')
|
||||
.then(r => this.services = r.data)
|
||||
.catch(e => {
|
||||
this.$bvToast.toast(`Не удалось загрузить сервисы: ${e}`, {
|
||||
title: 'Сбой',
|
||||
variant: 'danger',
|
||||
});
|
||||
console.error('Failed to load services:', e);
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCtfServices() {
|
||||
const instance = axios.create({
|
||||
baseURL: this.$store.state.apiUrl,
|
||||
auth: {
|
||||
username: this.$store.state.apiLogin,
|
||||
password: this.$store.state.apiPassword,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
instance.get('/service/')
|
||||
.then(response => (this.ctfServices = response.data))
|
||||
.catch(error => console.error('Failed to load CTF services!', error))
|
||||
},
|
||||
getPatterns() {
|
||||
const instance = axios.create({
|
||||
baseURL: this.$store.state.apiUrl,
|
||||
auth: {
|
||||
username: this.$store.state.apiLogin,
|
||||
password: this.$store.state.apiPassword,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
instance.get('/pattern/')
|
||||
.then(response => (this.patterns = response.data))
|
||||
.catch(error => console.error('Failed to load patterns!', error));
|
||||
components: {
|
||||
NavbarService,
|
||||
},
|
||||
createPattern() {
|
||||
const instance = axios.create({
|
||||
baseURL: this.$store.state.apiUrl,
|
||||
auth: {
|
||||
username: this.$store.state.apiLogin,
|
||||
password: this.$store.state.apiPassword,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
instance.post('/pattern/', this.newPattern)
|
||||
.then(this.getPatterns())
|
||||
.catch(error => console.error('failed to create pattern', error));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getCtfServices();
|
||||
this.getPatterns();
|
||||
|
||||
const dropdown = $('.dropdown');
|
||||
dropdown.on('show.bs.dropdown', function () {
|
||||
$(this).find('.dropdown-menu').first().stop(true, true).slideDown();
|
||||
});
|
||||
dropdown.on('hide.bs.dropdown', function () {
|
||||
$(this).find('.dropdown-menu').first().stop(true, true).slideUp();
|
||||
});
|
||||
},
|
||||
components: {FlagPattern, CtfService,},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
li {
|
||||
margin-left: 10px;
|
||||
.nav-link {
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
bottom: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
.navbar-cogs > i {
|
||||
transition: all .3s;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
min-width: 300px;
|
||||
.navbar-cogs > i:hover {
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 5px;
|
||||
.navbar-brand {
|
||||
padding-top: .5rem;
|
||||
padding-bottom: .5rem;
|
||||
|
||||
font-size: 1rem;
|
||||
background-color: rgba(0, 0, 0, .25);
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
nav {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 0.2em
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background: #F1F1F1
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #C1C1C1
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
29
src/components/NavbarService.vue
Normal file
29
src/components/NavbarService.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<router-link tag="li" class="nav-item text-nowrap edit-button" :to="{name:'stream', params: {servicePort: port}}">
|
||||
<a class="nav-link">{{name}} #{{port}}</a> <!-- TODO: editing! -->
|
||||
|
||||
<a class="nav-link" style="cursor: pointer"> <!-- TODO -->
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</a>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavbarService',
|
||||
props: {
|
||||
name: String(),
|
||||
port: Number(),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nav-link {
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
display: inherit;
|
||||
}
|
||||
</style>
|
||||
@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<div class="packet" :class="{'packet-incoming': isIncoming}">
|
||||
<div>#{{id}} at {{new Date(timestamp).toLocaleDateString('ru-RU', {month: '2-digit', day: '2-digit', hour:'2-digit', minute: '2-digit', second: '2-digit'})}}
|
||||
<button @click.prevent="copyRaw" class="btn btn-link">Copy HEX</button>
|
||||
<button @click.prevent="copyText" class="btn btn-link">Copy text</button>
|
||||
<button @click.prevent="copyPythonBytes" class="btn btn-link">Copy as Python bytes </button></div>
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-2 pb-2 mb-3 border-bottom">
|
||||
<pre>{{ hexdata }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Packet',
|
||||
props: ['id', 'content', 'timestamp', 'isIncoming',],
|
||||
methods: {
|
||||
hexdump(buffer, blockSize, lineNumberBase) {
|
||||
blockSize = parseInt(blockSize, 10) || 16;
|
||||
lineNumberBase = parseInt(lineNumberBase, 10) || 10;
|
||||
let lines = [];
|
||||
const hex = "0123456789ABCDEF";
|
||||
for (let b = 0; b < buffer.length; b += blockSize) {
|
||||
let block = buffer.slice(b, Math.min(b + blockSize, buffer.length));
|
||||
let addr = ("0000000000" + b.toString(lineNumberBase)).slice(-10);
|
||||
let codes = block.split('').map(ch => {
|
||||
let code = ch.charCodeAt(0);
|
||||
return " " + hex[(0xF0 & code) >> 4] + hex[0x0F & code];
|
||||
}).join("");
|
||||
codes += " ..".repeat(blockSize - block.length);
|
||||
// eslint-disable-next-line no-control-regex
|
||||
let chars = block.replace(/[\x00-\x1F]/g, '.');
|
||||
chars += " ".repeat(blockSize - block.length);
|
||||
lines.push(addr + ":" + codes + " |" + chars + "|");
|
||||
}
|
||||
return lines.join("\n");
|
||||
},
|
||||
copyPythonBytes() {
|
||||
const data = "b'" + atob(this.$props.content)
|
||||
.split('')
|
||||
.map((aChar) => {
|
||||
return '\\x' + ('0' + aChar.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join('') + "'";
|
||||
this.copyContent(data);
|
||||
},
|
||||
copyRaw() {
|
||||
this.copyContent(Buffer.from(this.$props.content, 'base64').toString('hex'));
|
||||
},
|
||||
copyText() {
|
||||
this.copyContent(atob(this.$props.content));
|
||||
},
|
||||
copyContent(data) {
|
||||
const tempEl = document.createElement('textarea');
|
||||
tempEl.value = data; // Chrome
|
||||
tempEl.textContent = data; // Firefox
|
||||
document.body.appendChild(tempEl);
|
||||
tempEl.select();
|
||||
const result = document.execCommand('copy');
|
||||
document.body.removeChild(tempEl);
|
||||
console.debug('Copy result is', result);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hexdata() {
|
||||
// const dataString = decodeURIComponent(escape(atob(this.$props.content)));
|
||||
const dataString = atob(this.$props.content);
|
||||
return this.hexdump(dataString, this.$store.state.hexdumpBlockSize, this.$store.state.hexdumpLineNumberBase);
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
pre {
|
||||
font-family: "Ubuntu Mono", "Lucida Console", monospace;
|
||||
font-size: 100%;
|
||||
color: black;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.packet {
|
||||
background: rgba(224, 246, 255, 0.4);
|
||||
box-shadow: 0 0 5px 5px rgba(224, 246, 255, 0.4);
|
||||
}
|
||||
.packet-incoming {
|
||||
background: rgba(251, 233, 231, 0.4);
|
||||
box-shadow: 0 0 5px 5px rgba(251, 233, 231, 0.4);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
top: -0.15em;
|
||||
position: relative;
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<div class="modal-mask">
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-container">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="inputApiUrl">URL бэкенда</label>
|
||||
<input type="url" class="form-control" id="inputApiUrl" placeholder="URL" v-model="apiUrl">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="inputApiLogin">Логин</label>
|
||||
<input type="text" class="form-control" id="inputApiLogin" v-model="apiLogin">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="inputApiPassword">Пароль</label>
|
||||
<input type="password" class="form-control" id="inputApiPassword" v-model="apiPassword">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-5">
|
||||
<label for="inputHexdumpBlockSize">Ширина блока HEX</label>
|
||||
<input type="number" class="form-control" id="inputHexdumpBlockSize" v-model="hexdumpBlockSize">
|
||||
</div>
|
||||
<div class="form-group col-md-7">
|
||||
<label for="inputHexdumpLineNumberBase">Номер строки</label>
|
||||
<select id="inputHexdumpLineNumberBase" class="form-control" v-model="hexdumpLineNumberBase">
|
||||
<option selected value="10">Десятичный</option>
|
||||
<option value="16">Шестнадцатеричный</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="inputPageSize">Размер страницы</label>
|
||||
<input type="number" class="form-control" id="inputPageSize" v-model="pageSize">
|
||||
</div>
|
||||
|
||||
<button type="submit" @click.prevent="$emit('close')" class="btn btn-primary">
|
||||
Закрыть
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SettingsModal",
|
||||
data: function () {
|
||||
return {
|
||||
escapeKeyCallback: (e) => {
|
||||
if (e.key === 'Escape') this.$emit('close');
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keyup', this.$data.escapeKeyCallback);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keyup', this.$data.escapeKeyCallback);
|
||||
},
|
||||
computed: {
|
||||
apiUrl: {
|
||||
get() {
|
||||
return this.$store.state.apiUrl;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setApiUrl', value);
|
||||
},
|
||||
},
|
||||
apiLogin: {
|
||||
get() {
|
||||
return this.$store.state.apiLogin;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setApiLogin', value);
|
||||
},
|
||||
},
|
||||
apiPassword: {
|
||||
get() {
|
||||
return this.$store.state.apiPassword;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setApiPassword', value);
|
||||
},
|
||||
},
|
||||
hexdumpBlockSize: {
|
||||
get() {
|
||||
return this.$store.state.hexdumpBlockSize;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setHexdumpBlockSize', value);
|
||||
},
|
||||
},
|
||||
hexdumpLineNumberBase: {
|
||||
get() {
|
||||
return this.$store.state.hexdumpLineNumberBase;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setHexdumpLineNumberBase', value);
|
||||
},
|
||||
},
|
||||
pageSize: {
|
||||
get() {
|
||||
return this.$store.state.pageSize;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setPageSize', value);
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
z-index: 9998;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
display: table;
|
||||
transition: opacity .3s ease;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 30px;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
|
||||
transition: all .3s ease;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
@ -1,222 +1,117 @@
|
||||
<template>
|
||||
<nav class="col-md-2 d-md-block bg-light sidebar">
|
||||
<nav class="col-sm-3 d-none d-sm-block bg-light sidebar">
|
||||
<div class="m-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-success">
|
||||
<i class="fas fa-pause"></i> <!-- TODO! -->
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm ml-1"
|
||||
:class="this.$store.state.displayFavoritesOnly ? 'btn-danger' : 'btn-outline-danger'"
|
||||
@click.stop.prevent="toggleFavorites">
|
||||
<i class="fas fa-star"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info" style="float: right;">
|
||||
<i class="fas fa-angle-double-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sidebar-sticky">
|
||||
<ul class="nav flex-column">
|
||||
<Stream
|
||||
v-for="stream in streams"
|
||||
:key="stream.id"
|
||||
:id="stream.id"
|
||||
:proto="stream.protocol"
|
||||
:servicePort="stream.service.port"
|
||||
:foundPatterns="stream.foundPatterns"
|
||||
:startTime="stream.startTimestamp"
|
||||
:endTime="stream.endTimestamp"
|
||||
:favorite="stream.favorite"
|
||||
:ttl="stream.ttl"></Stream>
|
||||
<infinite-loading @infinite="infiniteHandler" spinner="waveDots" ref="infiniteLoading"></infinite-loading>
|
||||
<!-- <li class="nav-item">-->
|
||||
<!-- <a class="nav-link" href="#">SOME ITEM</a>-->
|
||||
<!-- </li>-->
|
||||
<!-- <li class="nav-item">-->
|
||||
<!-- <a class="nav-link" href="#">SOME ITEM</a>-->
|
||||
<!-- </li>-->
|
||||
<!-- <li class="nav-item">-->
|
||||
<!-- <a class="nav-link" href="#">SOME ITEM</a>-->
|
||||
<!-- </li>-->
|
||||
<!-- <li class="nav-item">-->
|
||||
<!-- <a class="nav-link" href="#">SOME ITEM</a>-->
|
||||
<!-- </li>-->
|
||||
<SidebarStream v-for="stream in streams"
|
||||
:key="stream.id"
|
||||
:stream="stream"></SidebarStream>
|
||||
<infinite-loading @infinite="infiniteLoadingHandler" ref="infiniteLoader"></infinite-loading>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Stream from "@/components/Stream.vue";
|
||||
import InfiniteLoading from "vue-infinite-loading";
|
||||
import axios from "axios";
|
||||
import SidebarStream from './SidebarStream';
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
props: ['servicePort', 'streamId',],
|
||||
data: function () {
|
||||
return {
|
||||
streams: [],
|
||||
navigationKeysCallback: (e) => {
|
||||
if (!e.ctrlKey) return;
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const index = this.$data.streams.findIndex(e => e.id === this.$props.streamId);
|
||||
const newStream = this.$data.streams[index - 1];
|
||||
if (!newStream) return;
|
||||
const newId = newStream.id;
|
||||
this.$router.push({
|
||||
name: 'home',
|
||||
params: {servicePort: this.$props.servicePort, streamId: newId,},
|
||||
});
|
||||
document.getElementById(`stream-${newId}`).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
});
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
const index = this.$data.streams.findIndex(e => e.id === this.$props.streamId);
|
||||
const newStream = this.$data.streams[index + 1];
|
||||
if (!newStream) return;
|
||||
const newId = newStream.id;
|
||||
this.$router.push({
|
||||
name: 'home',
|
||||
params: {servicePort: this.$props.servicePort, streamId: newId,},
|
||||
});
|
||||
document.getElementById(`stream-${newId}`).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
});
|
||||
} else if (e.key === 'Home') {
|
||||
const newStream = this.$data.streams[0];
|
||||
if (!newStream) return;
|
||||
const newId = newStream.id;
|
||||
this.$router.push({
|
||||
name: 'home',
|
||||
params: {servicePort: this.$props.servicePort, streamId: newId,},
|
||||
});
|
||||
document.getElementById(`stream-${newId}`).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
});
|
||||
} else if (e.key === 'End') {
|
||||
const newStream = this.$data.streams[this.$data.streams.length - 1];
|
||||
if (!newStream) return;
|
||||
const newId = newStream.id;
|
||||
this.$router.push({
|
||||
name: 'home',
|
||||
params: {servicePort: this.$props.servicePort, streamId: newId,},
|
||||
});
|
||||
document.getElementById(`stream-${newId}`).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'servicePort': function () {
|
||||
this.rerender();
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
props: ['servicePort', 'streamId',],
|
||||
data() {
|
||||
return {
|
||||
streams: [],
|
||||
};
|
||||
},
|
||||
'$route.query.pId': function () {
|
||||
this.rerender();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
infiniteHandler($state) {
|
||||
const instance = axios.create({
|
||||
baseURL: this.$store.state.apiUrl,
|
||||
auth: {
|
||||
username: this.$store.state.apiLogin,
|
||||
password: this.$store.state.apiPassword,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const streams = this.streams;
|
||||
let startsFrom;
|
||||
if (streams && streams.length && streams[streams.length - 1]) {
|
||||
startsFrom = streams[streams.length - 1].id;
|
||||
} else {
|
||||
startsFrom = Number.MAX_SAFE_INTEGER; // FIXME -_-
|
||||
}
|
||||
|
||||
instance.post(`/stream/${this.$props.servicePort || 'all'}`, {
|
||||
direction: 'DESC',
|
||||
startingFrom: startsFrom,
|
||||
pageSize: parseInt(this.$store.state.pageSize),
|
||||
pattern: this.$route.query.pId ? {id: this.$route.query.pId,} : null,
|
||||
favorites: this.$store.state.displayFavOnly,
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
if (data.length === 0) return $state.complete();
|
||||
if (data[0] && this.streams[0] && data[0].id === this.streams[0].id) { // FIXME: проверка довольно колхозная
|
||||
$state.complete();
|
||||
methods: {
|
||||
infiniteLoadingHandler($state) {
|
||||
const ourStreams = this.streams;
|
||||
let startsFrom;
|
||||
if (ourStreams?.length && ourStreams[ourStreams.length - 1]) {
|
||||
startsFrom = ourStreams[ourStreams.length - 1].id;
|
||||
} else {
|
||||
this.streams.push(...data);
|
||||
$state.loaded();
|
||||
startsFrom = Number.MAX_SAFE_INTEGER; // FIXME: rewrite backend?
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Failed to load new portion of streams', error);
|
||||
return $state.error();
|
||||
});
|
||||
|
||||
this.$http.post(`/stream/${this.$props.servicePort || 'all'}`, {
|
||||
direction: 'DESC',
|
||||
startingFrom: startsFrom,
|
||||
pageSize: this.$store.state.pageSize,
|
||||
pattern: null, // TODO
|
||||
favorites: this.$store.state.displayFavoritesOnly,
|
||||
}).then(r => {
|
||||
const data = r.data;
|
||||
if (data?.length === 0) return $state.complete();
|
||||
if (data[0]?.id === this.streams[0]?.id) {
|
||||
return $state.complete();
|
||||
}
|
||||
|
||||
this.streams.push(...data);
|
||||
return $state.loaded();
|
||||
}).catch(e => {
|
||||
console.error('Failed to load portion of streams:', e);
|
||||
return $state.error();
|
||||
});
|
||||
},
|
||||
toggleFavorites() {
|
||||
this.$store.commit('toggleDisplayFavoritesOnly');
|
||||
},
|
||||
},
|
||||
onGotNewStream(stream) {
|
||||
if (this.$route.query.pId && !stream.foundPatterns.some(e => e.id === this.$route.query.pId)) {
|
||||
return;
|
||||
}
|
||||
if (this.streams.length > this.$store.state.pageSize * 2) {
|
||||
this.streams.pop();
|
||||
}
|
||||
this.streams.unshift(stream);
|
||||
components: {
|
||||
SidebarStream,
|
||||
// InfiniteLoading,
|
||||
},
|
||||
rerender() {
|
||||
this.streams = [];
|
||||
this.$refs.infiniteLoading.stateChanger.reset();
|
||||
},
|
||||
},
|
||||
components: {Stream, InfiniteLoading,},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.$data.navigationKeysCallback);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.$data.navigationKeysCallback);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
display: block !important;
|
||||
flex: 0 0 16.666667%;
|
||||
max-width: 20.666667%;
|
||||
position: fixed !important;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
padding: 40px 0 0 0 !important;
|
||||
padding: 40px 0 0; /* Height of navbar */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
top: 0;
|
||||
height: calc(100vh - 40px);
|
||||
height: calc(100vh - 85px);
|
||||
/*padding-top: .5rem;*/
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
}
|
||||
|
||||
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
||||
@supports (position: sticky) {
|
||||
.sidebar-sticky {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .feather {
|
||||
margin-right: 4px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover .feather,
|
||||
.sidebar .nav-link.active .feather {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
|
||||
23
src/components/SidebarStream.vue
Normal file
23
src/components/SidebarStream.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Huh?!</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SidebarStream',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/*noinspection CssUnusedSymbol*/
|
||||
.nav-link.active {
|
||||
color: #007BFF;
|
||||
}
|
||||
</style>
|
||||
@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<li :id="`stream-${id}`" class="nav-item" :class="{highlight: favorite0}" style="transition: all .5s;">
|
||||
<router-link class="nav-link" active-class="active"
|
||||
:to="{ name: 'home', params: { servicePort: servicePort, streamId: id}, query: $route.query }">
|
||||
<button class="btn-text" @click.prevent="toggleStar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32" width="16" height="16" fill="none" class="star" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M16 2 L20 12 30 12 22 19 25 30 16 23 7 30 10 19 2 12 12 12 Z" style="transition: all .5s;" :fill="favorite0 ? 'red' : 'white'"></path>
|
||||
</svg>
|
||||
</button>
|
||||
#{{id}} {{proto}} <span v-if="ttl">TTL {{ttl}}</span><br/>
|
||||
{{new Date(startTime).toLocaleDateString('ru-RU', {month: '2-digit', day: '2-digit', hour:'2-digit', minute: '2-digit', second: '2-digit'})}}
|
||||
- {{new Date(endTime).toLocaleTimeString('ru-RU', {hour:'2-digit', minute: '2-digit', second: '2-digit'})}}
|
||||
<span v-for="pattern in foundPatterns"
|
||||
:key="pattern.id"
|
||||
:style="'color:' + pattern.color"
|
||||
>
|
||||
{{pattern.name}}
|
||||
</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
name: "Stream",
|
||||
props: ['id', 'proto', 'startTime', 'endTime', 'servicePort', 'foundPatterns', 'favorite', 'ttl',],
|
||||
data: function () {
|
||||
return {
|
||||
favorite0: this.favorite,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleStar() {
|
||||
const instance = axios.create({
|
||||
baseURL: this.$store.state.apiUrl,
|
||||
auth: {
|
||||
username: this.$store.state.apiLogin,
|
||||
password: this.$store.state.apiPassword,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
instance.post(`/stream/${this.id}/${this.favorite ? 'unfavorite' : 'favorite'}`)
|
||||
.then(() => this.favorite0 = !this.favorite0)
|
||||
.catch(e => console.error('Failed to toggle favorite!', e));
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
padding: 0;
|
||||
top: -0.1em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .feather {
|
||||
margin-right: 4px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover .feather,
|
||||
.sidebar .nav-link.active .feather {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
border: none;
|
||||
background-color: inherit;
|
||||
/*padding: 14px 28px;*/
|
||||
/*font-size: 16px;*/
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.star {
|
||||
stroke: #bfbfbf;
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.star:hover {
|
||||
stroke: red;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: rgba(255, 119, 182, 0.3);
|
||||
box-shadow: 0 0 1px 1px rgba(255, 119, 182, 0.3);
|
||||
}
|
||||
</style>
|
||||
51
src/main.js
51
src/main.js
@ -1,17 +1,50 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import 'bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import Vue from 'vue';
|
||||
import BootstrapVue from 'bootstrap-vue';
|
||||
import Axios from 'axios';
|
||||
import InfiniteLoading from 'vue-infinite-loading';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
import store from './store';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css';
|
||||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
Vue.use(BootstrapVue);
|
||||
Vue.use(InfiniteLoading, {
|
||||
slots: {
|
||||
'no-results': 'Нет результатов...',
|
||||
'no-more': 'Данных больше нет',
|
||||
error: 'Произошла ошибка!',
|
||||
errorBtnText: 'Повторить',
|
||||
},
|
||||
props: {
|
||||
spinner: 'waveDots',
|
||||
},
|
||||
});
|
||||
|
||||
const axiosInstance = Axios.create({
|
||||
// baseURL: '/api', // TODO: edit for release!
|
||||
baseURL: 'http://192.168.79.131:8080/api',
|
||||
auth: {
|
||||
username: store.state.apiLogin,
|
||||
password: store.state.apiPassword,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 3000,
|
||||
withCredentials: true,
|
||||
maxContentLength: 20000,
|
||||
});
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
Vue.prototype.$http = axiosInstance;
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App),
|
||||
}).$mount('#app');
|
||||
|
||||
|
||||
// TODO: вынести наконец инстанс axios сюда!
|
||||
|
||||
@ -1,16 +1,32 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Home from './views/Home.vue'
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import ContentStream from './views/ContentStream';
|
||||
import Sidebar from './components/Sidebar';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export default new Router({
|
||||
routes: [
|
||||
// {
|
||||
// path: '/',
|
||||
// name: 'nothing',
|
||||
// component: ContentNothing,
|
||||
// },
|
||||
{
|
||||
path: '/:servicePort?/:streamId?',
|
||||
name: 'home',
|
||||
component: Home,
|
||||
name: 'stream',
|
||||
props: true,
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
// component: () => import(/* webpackChunkName: "stream" */ './views/ContentStream'),
|
||||
|
||||
// component: ContentStream,
|
||||
components: {
|
||||
sidebar: Sidebar,
|
||||
content: ContentStream,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
linkActiveClass: 'active',
|
||||
});
|
||||
|
||||
39
src/store.js
39
src/store.js
@ -1,27 +1,38 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import createPersistedState from 'vuex-persistedstate'
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import createMutationsSharer from 'vuex-shared-mutations';
|
||||
import createPersistedState from 'vuex-persistedstate';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
// noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
apiUrl: 'http://127.0.0.1/api',
|
||||
apiLogin: 'BinaryBears',
|
||||
apiPassword: '123456',
|
||||
hexdumpBlockSize: 16,
|
||||
hexdumpLineNumberBase: 10,
|
||||
pageSize: 25,
|
||||
displayFavOnly: false,
|
||||
displayFavoritesOnly: false,
|
||||
},
|
||||
mutations: {
|
||||
setApiUrl: (state, payload) => state.apiUrl = payload,
|
||||
setApiLogin: (state, payload) => state.apiLogin = payload,
|
||||
setApiPassword: (state, payload) => state.apiPassword = payload,
|
||||
setHexdumpBlockSize: (state, payload) => state.hexdumpBlockSize = payload,
|
||||
setHexdumpLineNumberBase: (state, payload) => state.hexdumpLineNumberBase = payload,
|
||||
setPageSize: (state, payload) => state.pageSize = payload,
|
||||
setDisplayFavOnly: (state, payload) => state.displayFavOnly = payload,
|
||||
setApiLogin: (s, p) => s.apiLogin = p,
|
||||
setApiPassword: (s, p) => s.apiPassword = p,
|
||||
setHexdumpBlockSize: (s, p) => s.hexdumpBlockSize = p,
|
||||
setHexdumpLineNumberBase: (s, p) => s.hexdumpLineNumberBase = p,
|
||||
setPageSize: (s, p) => s.pageSize = p,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
toggleDisplayFavoritesOnly: (s, p) => s.displayFavoritesOnly = !s.displayFavoritesOnly,
|
||||
},
|
||||
plugins: [createPersistedState(),],
|
||||
})
|
||||
actions: {},
|
||||
plugins: [
|
||||
createPersistedState(),
|
||||
createMutationsSharer({
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
predicate: (mutation, state) => {
|
||||
console.debug('Got mutation:', mutation);
|
||||
return mutation?.type !== 'setDisplayFavoritesOnly';
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
16
src/views/ContentStream.vue
Normal file
16
src/views/ContentStream.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
Some stream #{{ streamId }} at port #{{ servicePort }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['servicePort', 'streamId',],
|
||||
name: 'ContentStream',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Navbar @settingsButtonClicked="showSettingsModal = true" @addServiceButtonClicked="showAddServiceModal = true" ref="navbar"></Navbar>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<Sidebar ref="sidebar" :servicePort="servicePort" :streamId="streamId"></Sidebar>
|
||||
<!--suppress HtmlDeprecatedTag -->
|
||||
<Content :servicePort="servicePort" :streamId="streamId" @displayFavChanged="reloadSidebar()"></Content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="modal">
|
||||
<SettingsModal v-if="showSettingsModal" @close="showSettingsModal = false"></SettingsModal>
|
||||
</transition>
|
||||
<transition name="modal">
|
||||
<AddServiceModal v-if="showAddServiceModal"
|
||||
@serviceAddDone="$refs.navbar.ctfServices.push($event); showAddServiceModal = false"
|
||||
@close="showAddServiceModal = false"></AddServiceModal>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingsModal from "@/components/SettingsModal.vue";
|
||||
import Navbar from "@/components/Navbar.vue";
|
||||
import Sidebar from "@/components/Sidebar.vue";
|
||||
import Content from "@/components/Content.vue";
|
||||
import AddServiceModal from "@/components/AddServiceModal.vue";
|
||||
import SockJS from "sockjs-client";
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
data: function () {
|
||||
return {
|
||||
showSettingsModal: false,
|
||||
showAddServiceModal: false,
|
||||
socket: null,
|
||||
navbarTimer: null,
|
||||
}
|
||||
},
|
||||
props: ['servicePort', 'streamId',],
|
||||
methods: {
|
||||
reloadSidebar() {
|
||||
this.$refs.sidebar.rerender();
|
||||
},
|
||||
reloadNavbar() {
|
||||
this.$refs.navbar.getCtfServices();
|
||||
this.$refs.navbar.getPatterns();
|
||||
},
|
||||
connectWs() {
|
||||
const wsUrl = this.$store.state.apiUrl + '/ws';
|
||||
// FIXME: нельзя посылать заголовок авторизации (может появиться окно с авторизацией, что не круто),
|
||||
// FIXME: нужно либо убрать его на эндпоинте, либо использовать другой способ
|
||||
// FIXME: (https://groups.google.com/forum/#!topic/sockjs/4gmZy8XZFIY)
|
||||
this.socket = new SockJS(wsUrl);
|
||||
this.socket.onopen = function () {
|
||||
console.info('WS connected');
|
||||
};
|
||||
this.socket.onclose = function (ev) {
|
||||
console.info('WS disconnected', ev.code, ev.reason);
|
||||
if (ev.code === 1008) { // FIXME: выключить security timeout (This connection was established under an authenticated HTTP session that has ended.)
|
||||
console.info('Security timeout, reconnecting...');
|
||||
this.connectWs();
|
||||
}
|
||||
};
|
||||
this.socket.onmessage = ev => {
|
||||
const parsed = JSON.parse(ev.data);
|
||||
const currentPort = parseInt(this.$route.params.servicePort, 10);
|
||||
if (!(currentPort === undefined || currentPort === parsed.service.port)) {
|
||||
return;
|
||||
}
|
||||
if (!this.$store.state.displayFavOnly) {
|
||||
this.$refs.sidebar.onGotNewStream(parsed);
|
||||
}
|
||||
};
|
||||
this.socket.onerror = function (ev) {
|
||||
console.warn('WS err', ev);
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.connectWs();
|
||||
this.navbarTimer = setInterval(this.reloadNavbar, 5000); // TODO: некрасиво, лучше ловить через WS
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.socket.close();
|
||||
clearInterval(this.navbarTimer);
|
||||
},
|
||||
components: {
|
||||
AddServiceModal,
|
||||
SettingsModal,
|
||||
Navbar,
|
||||
Sidebar,
|
||||
Content,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave-active .modal-container {
|
||||
-webkit-transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
102
src/views/Settings.vue
Normal file
102
src/views/Settings.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<!--suppress JSUnresolvedVariable -->
|
||||
<template>
|
||||
<b-modal id="settingsModal" title="Настройки" centered scrollable ok-only>
|
||||
<b-form-group
|
||||
label-cols-sm="4"
|
||||
label-cols-lg="3"
|
||||
description="Логин для подключения к API"
|
||||
label="Логин"
|
||||
label-for="settings-apiLogin">
|
||||
<b-form-input id="settings-apiLogin" v-model="apiLogin"></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
label-cols-sm="4"
|
||||
label-cols-lg="3"
|
||||
description="Пароль для подключения к API"
|
||||
label="Пароль"
|
||||
label-for="settings-apiPassword">
|
||||
<b-form-input type="password" id="settings-apiPassword" v-model="apiPassword"></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
label-cols-sm="4"
|
||||
label-cols-lg="3"
|
||||
description="Количество байт для отображения в бинарном представлении пакета"
|
||||
label="Ширина блока HEX"
|
||||
label-for="settings-hexdumpBlockSize">
|
||||
<b-form-input type="number" id="settings-hexdumpBlockSize" v-model.number="hexdumpBlockSize"></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
label-cols-sm="4"
|
||||
label-cols-lg="3"
|
||||
description="Система счисления, используемая в номерах строк бинарного представления пакета"
|
||||
label="Нумерация строк"
|
||||
label-for="settings-hexdumpLineNumberBase">
|
||||
<b-form-select id="settings-hexdumpLineNumberBase" v-model="hexdumpLineNumberBase">
|
||||
<option :value="10" selected>Десятичная</option>
|
||||
<option :value="16">Шестнадцатеричная</option>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
label-cols-sm="4"
|
||||
label-cols-lg="3"
|
||||
description="Количество стримов для загрузки за раз"
|
||||
label="Размер страницы"
|
||||
label-for="settings-pageSize">
|
||||
<b-form-input type="number" id="settings-pageSize" v-model.number="pageSize"></b-form-input>
|
||||
</b-form-group>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Settings',
|
||||
computed: {
|
||||
apiLogin: {
|
||||
get() {
|
||||
return this.$store.state.apiLogin;
|
||||
},
|
||||
set(v) {
|
||||
this.$store.commit('setApiLogin', v);
|
||||
this.$http.defaults.auth.username = v;
|
||||
},
|
||||
},
|
||||
apiPassword: {
|
||||
get() {
|
||||
return this.$store.state.apiPassword;
|
||||
},
|
||||
set(v) {
|
||||
this.$store.commit('setApiPassword', v);
|
||||
this.$http.defaults.auth.password = v;
|
||||
},
|
||||
},
|
||||
hexdumpBlockSize: {
|
||||
get() {
|
||||
return this.$store.state.hexdumpBlockSize;
|
||||
},
|
||||
set(v) {
|
||||
this.$store.commit('setHexdumpBlockSize', v);
|
||||
},
|
||||
},
|
||||
hexdumpLineNumberBase: {
|
||||
get() {
|
||||
return this.$store.state.hexdumpLineNumberBase;
|
||||
},
|
||||
set(v) {
|
||||
this.$store.commit('setHexdumpLineNumberBase', v);
|
||||
},
|
||||
},
|
||||
pageSize: {
|
||||
get() {
|
||||
return this.$store.state.pageSize;
|
||||
},
|
||||
set(v) {
|
||||
this.$store.commit('setPageSize', v);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@ -1,3 +1,3 @@
|
||||
module.exports = {
|
||||
publicPath: '',
|
||||
lintOnSave: false,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user