Start the dashboard

pull/144/head
Henrique Dias 2017-07-06 21:18:34 +01:00
parent 7ec3660a09
commit ba7c3d4234
No known key found for this signature in database
GPG Key ID: 936F5EB68D786730
14 changed files with 570 additions and 256 deletions

View File

@ -6,6 +6,7 @@
export default {
name: 'app',
mounted: function () {
// Remove loading animation.
let loading = document.getElementById('loading')
loading.classList.add('done')

View File

@ -0,0 +1,171 @@
<template>
<div v-if="error">
<h2 class="message" v-if="error === 404">
<i class="material-icons">gps_off</i>
<span>This location can't be reached.</span>
</h2>
<h2 class="message" v-else-if="error === 403">
<i class="material-icons">error</i>
<span>You're not welcome here.</span>
</h2>
<h2 class="message" v-else>
<i class="material-icons">error_outline</i>
<span>Something really went wrong.</span>
</h2>
</div>
<editor v-else-if="isEditor"></editor>
<listing :class="{ multiple }" v-else-if="isListing"></listing>
<preview v-else-if="isPreview"></preview>
</template>
<script>
import Preview from './Preview'
import Listing from './Listing'
import Editor from './Editor'
import css from '@/utils/css'
import api from '@/utils/api'
import { mapGetters, mapState, mapMutations } from 'vuex'
function updateColumnSizes () {
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
let items = css(['#listing.mosaic .item', '.mosaic#listing .item'])
if (columns === 0) columns = 1
items.style.width = `calc(${100 / columns}% - 1em)`
}
export default {
name: 'files',
components: {
Preview,
Listing,
Editor
},
computed: {
...mapGetters([
'selectedCount'
]),
...mapState([
'req',
'user',
'reload',
'multiple',
'loading'
]),
isListing () {
return this.req.kind === 'listing' && !this.loading
},
isPreview () {
return this.req.kind === 'preview' && !this.loading
},
isEditor () {
return this.req.kind === 'editor' && !this.loading
}
},
data: function () {
return {
error: null
}
},
created () {
this.fetchData()
console.log('created')
},
watch: {
'$route': 'fetchData',
'reload': function () {
this.$store.commit('setReload', false)
this.fetchData()
}
},
mounted () {
updateColumnSizes()
window.addEventListener('resize', updateColumnSizes)
window.addEventListener('keydown', this.keyEvent)
},
methods: {
...mapMutations([ 'setLoading' ]),
fetchData () {
// Set loading to true and reset the error.
this.setLoading(true)
this.error = null
let url = this.$route.path
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url
api.fetch(url)
.then((trueURL) => {
if (!url.endsWith('/') && trueURL.endsWith('/')) {
console.log(trueURL)
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
}
this.setLoading(false)
})
.catch(error => {
console.log(error)
this.error = error
this.setLoading(false)
})
},
keyEvent (event) {
// Esc!
if (event.keyCode === 27) {
this.$store.commit('closeHovers')
if (this.req.kind !== 'listing') {
return
}
// If we're on a listing, unselect all files and folders.
let items = document.getElementsByClassName('item')
Array.from(items).forEach(link => {
link.setAttribute('aria-selected', false)
})
this.$store.commit('resetSelected')
}
// Del!
if (event.keyCode === 46) {
if (this.showDeleteButton && this.req.kind !== 'editor') {
this.$store.commit('showHover', 'delete')
}
}
// F1!
if (event.keyCode === 112) {
event.preventDefault()
this.$store.commit('showHover', 'help')
}
// F2!
if (event.keyCode === 113) {
if (this.showRenameButton) {
this.$store.commit('showHover', 'rename')
}
}
// CTRL + S
if (event.ctrlKey || event.metaKey) {
if (String.fromCharCode(event.which).toLowerCase() === 's') {
event.preventDefault()
if (this.req.kind !== 'editor') {
document.getElementById('download-button').click()
return
}
}
}
},
openSidebar () {
this.$store.commit('showHover', 'sidebar')
},
openSearch () {
this.$store.commit('showHover', 'search')
}
}
}
</script>

View File

@ -0,0 +1,135 @@
<template>
<header>
<div>
<button @click="openSidebar" aria-label="Toggle sidebar" title="Toggle sidebar" class="action">
<i class="material-icons">menu</i>
</button>
<img src="../assets/logo.svg" alt="File Manager">
<search></search>
</div>
<div>
<button @click="openSearch" aria-label="Search" title="Search" class="search-button action">
<i class="material-icons">search</i>
</button>
<button v-show="showSaveButton" aria-label="Save" class="action" id="save-button">
<i class="material-icons" title="Save">save</i>
</button>
<rename-button v-show="showRenameButton"></rename-button>
<move-button v-show="showMoveButton"></move-button>
<delete-button v-show="showDeleteButton"></delete-button>
<switch-button v-show="showSwitchButton"></switch-button>
<download-button v-show="showCommonButton"></download-button>
<upload-button v-show="showUpload"></upload-button>
<info-button v-show="showCommonButton"></info-button>
<button v-show="showSelectButton" @click="$store.commit('multiple', true)" aria-label="Select multiple" class="action">
<i class="material-icons">check_circle</i>
<span>Select</span>
</button>
</div>
</header>
</template>
<script>
import Search from './Search'
import InfoButton from './buttons/Info'
import DeleteButton from './buttons/Delete'
import RenameButton from './buttons/Rename'
import UploadButton from './buttons/Upload'
import DownloadButton from './buttons/Download'
import SwitchButton from './buttons/SwitchView'
import MoveButton from './buttons/Move'
import {mapGetters, mapState} from 'vuex'
export default {
name: 'main',
components: {
Search,
InfoButton,
DeleteButton,
RenameButton,
DownloadButton,
UploadButton,
SwitchButton,
MoveButton
},
computed: {
...mapGetters([
'selectedCount'
]),
...mapState([
'req',
'user',
'loading',
'reload',
'multiple'
]),
showSelectButton () {
return this.req.kind === 'listing' && !this.loading && this.$route.name === 'Files'
},
showSaveButton () {
return (this.req.kind === 'editor' && !this.loading) || this.$route.name === 'User'
},
showSwitchButton () {
return this.req.kind === 'listing' && this.$route.name === 'Files' && !this.loading
},
showCommonButton () {
return !(this.$route.name !== 'Files' || this.loading)
},
showUpload () {
if (this.$route.name !== 'Files' || this.loading) return false
if (this.req.kind === 'editor') return false
return this.user.allowNew
},
showDeleteButton () {
if (this.$route.name !== 'Files' || this.loading) return false
if (this.req.kind === 'listing') {
if (this.selectedCount === 0) {
return false
}
return this.user.allowEdit
}
return this.user.allowEdit
},
showRenameButton () {
if (this.$route.name !== 'Files' || this.loading) return false
if (this.req.kind === 'listing') {
if (this.selectedCount === 1) {
return this.user.allowEdit
}
return false
}
return this.user.allowEdit
},
showMoveButton () {
if (this.$route.name !== 'Files' || this.loading) return false
if (this.req.kind !== 'listing') {
return false
}
if (this.selectedCount > 0) {
return this.user.allowEdit
}
return false
}
},
methods: {
openSidebar () {
this.$store.commit('showHover', 'sidebar')
},
openSearch () {
this.$store.commit('showHover', 'search')
}
}
}
</script>

View File

@ -1,273 +1,34 @@
<template>
<div :class="{ multiple, loading }">
<header>
<div>
<button @click="openSidebar" aria-label="Toggle sidebar" title="Toggle sidebar" class="action">
<i class="material-icons">menu</i>
</button>
<img src="../assets/logo.svg" alt="File Manager">
<search></search>
</div>
<div>
<button @click="openSearch" aria-label="Search" title="Search" class="search-button action">
<i class="material-icons">search</i>
</button>
<button v-show="isEditor" aria-label="Save" class="action" id="save-button">
<i class="material-icons" title="Save">save</i>
</button>
<rename-button v-show="!loading && showRenameButton"></rename-button>
<move-button v-show="!loading && showMoveButton"></move-button>
<delete-button v-show="!loading && showDeleteButton"></delete-button>
<switch-button v-show="!loading && req.kind !== 'editor'"></switch-button>
<download-button></download-button>
<upload-button v-show="!loading && showUpload"></upload-button>
<info-button></info-button>
<button v-show="isListing" @click="$store.commit('multiple', true)" aria-label="Select multiple" class="action">
<i class="material-icons">check_circle</i>
<span>Select</span>
</button>
</div>
</header>
<div>
<site-header></site-header>
<sidebar></sidebar>
<main>
<div v-if="loading">
<h2 class="message">
<span>Loading...</span>
</h2>
</div>
<div v-else-if="error">
<h2 class="message" v-if="error === 404">
<i class="material-icons">gps_off</i>
<span>This location can't be reached.</span>
</h2>
<h2 class="message" v-else-if="error === 403">
<i class="material-icons">error</i>
<span>You're not welcome here.</span>
</h2>
<h2 class="message" v-else>
<i class="material-icons">error_outline</i>
<span>Something really went wrong.</span>
</h2>
</div>
<editor v-else-if="isEditor"></editor>
<listing v-else-if="isListing"></listing>
<preview v-else-if="isPreview"></preview>
<router-view></router-view>
</main>
<prompts></prompts>
</div>
</template>
<script>
import Search from './Search'
import Preview from './Preview'
import Listing from './Listing'
import Editor from './Editor'
import Sidebar from './Sidebar'
import Prompts from './prompts/Prompts'
import InfoButton from './buttons/Info'
import DeleteButton from './buttons/Delete'
import RenameButton from './buttons/Rename'
import UploadButton from './buttons/Upload'
import DownloadButton from './buttons/Download'
import SwitchButton from './buttons/SwitchView'
import MoveButton from './buttons/Move'
import css from '@/utils/css'
import api from '@/utils/api'
import {mapGetters, mapState} from 'vuex'
function updateColumnSizes () {
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
let items = css(['#listing.mosaic .item', '.mosaic#listing .item'])
if (columns === 0) columns = 1
items.style.width = `calc(${100 / columns}% - 1em)`
}
import SiteHeader from './Header'
export default {
name: 'main',
components: {
Search,
Preview,
Listing,
Editor,
Sidebar,
InfoButton,
DeleteButton,
RenameButton,
DownloadButton,
UploadButton,
SwitchButton,
MoveButton,
SiteHeader,
Prompts
},
computed: {
...mapGetters([
'selectedCount'
]),
...mapState([
'req',
'user',
'reload',
'multiple'
]),
isListing () {
return this.req.kind === 'listing' && !this.loading
},
isPreview () {
return this.req.kind === 'preview' && !this.loading
},
isEditor () {
return this.req.kind === 'editor' && !this.loading
},
showUpload () {
if (this.req.kind === 'editor') return false
return this.user.allowNew
},
showDeleteButton () {
if (this.req.kind === 'listing') {
if (this.selectedCount === 0) {
return false
}
return this.user.allowEdit
}
return this.user.allowEdit
},
showRenameButton () {
if (this.req.kind === 'listing') {
if (this.selectedCount === 1) {
return this.user.allowEdit
}
return false
}
return this.user.allowEdit
},
showMoveButton () {
if (this.req.kind !== 'listing') {
return false
}
if (this.selectedCount > 0) {
return this.user.allowEdit
}
return false
}
},
data: function () {
return {
loading: true,
error: null
}
},
created () {
this.fetchData()
},
watch: {
'$route': 'fetchData',
'reload': function () {
this.$store.commit('setReload', false)
this.fetchData()
}
},
mounted () {
updateColumnSizes()
window.addEventListener('resize', updateColumnSizes)
window.addEventListener('keydown', this.keyEvent)
},
methods: {
fetchData () {
// Set loading to true and reset the error.
this.loading = true
this.error = null
'$route': function () {
// Reset selected items and multiple selection.
this.$store.commit('resetSelected')
this.$store.commit('multiple', false)
this.$store.commit('closeHovers')
let url = this.$route.path
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url
api.fetch(url)
.then((trueURL) => {
if (!url.endsWith('/') && trueURL.endsWith('/')) {
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
}
this.loading = false
})
.catch(error => {
console.log(error)
this.error = error
this.loading = false
})
},
keyEvent (event) {
// Esc!
if (event.keyCode === 27) {
this.$store.commit('closeHovers')
if (this.req.kind !== 'listing') {
return
}
// If we're on a listing, unselect all files and folders.
let items = document.getElementsByClassName('item')
Array.from(items).forEach(link => {
link.setAttribute('aria-selected', false)
})
this.$store.commit('resetSelected')
}
// Del!
if (event.keyCode === 46) {
if (this.showDeleteButton && this.req.kind !== 'editor') {
this.$store.commit('showHover', 'delete')
}
}
// F1!
if (event.keyCode === 112) {
event.preventDefault()
this.$store.commit('showHover', 'help')
}
// F2!
if (event.keyCode === 113) {
if (this.showRenameButton) {
this.$store.commit('showHover', 'rename')
}
}
// CTRL + S
if (event.ctrlKey || event.metaKey) {
if (String.fromCharCode(event.which).toLowerCase() === 's') {
event.preventDefault()
if (this.req.kind !== 'editor') {
document.getElementById('download-button').click()
return
}
}
}
},
openSidebar () {
this.$store.commit('showHover', 'sidebar')
},
openSearch () {
this.$store.commit('showHover', 'search')
}
}
}

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Settings</h1>
</div>
</template>
<script>
export default {
name: 'settings'
}
</script>

View File

@ -25,7 +25,7 @@
</div>
<div>
<router-link class="action" to="/dashboard" aria-label="Settings" title="Settings">
<router-link class="action" to="/settings" aria-label="Settings" title="Settings">
<i class="material-icons">settings_applications</i>
<span>Settings</span>
</router-link>

View File

@ -0,0 +1,177 @@
<template>
<div class="dashboard">
<h1>User</h1>
<p><label for="username">Username</label><input type="text" v-model="username" name="username"></p>
<p><label for="password">Password</label><input type="password" :disabled="passwordBlock" v-model="password" name="password"></p>
<p><label for="scope">Scope</label><input type="text" v-model="scope" name="scope"></p>
<hr>
<h2>Permissions</h2>
<p class="small">You can set the user to be an administrator or choose the permissions individually.
If you select "Administrator", all of the other options will be automatically checked.
The management of users remains a privilege of an administrator.</p>
<p><input type="checkbox" v-model="admin"> Administrator</p>
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> Create new files and directories</p>
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> Edit, rename and delete files or directories.</p>
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> Execute commands</p>
<h3>Commands</h3>
<p class="small">A space separated list with the available commands for this user. Example: <i>git svn hg</i>.</p>
<input type="text" v-model="commands">
<hr>
<h2>Rules</h2>
<p class="small">Here you can define a set of allow and disallow rules for this specific user. The blocked files won't
show up in the listings and they won't be accessible to the user. We support regex and paths relative to
the user's scope.</p>
<p class="small">Each rule goes in one different line and must start with the keyword <code>allow</code> or <code>disallow</code>.
Then you should write <code>regex</code> if you are using a regular expression and then the expression or the path.</p>
<p class="small"><strong>Examples</strong></p>
<ul class="small">
<li><code>disallow regex \\/\\..+</code> - prevents the access to any dot file (such as .git, .gitignore) in every folder.</li>
<li><code>disallow /Caddyfile</code> - blocks the access to the file named <i>Caddyfile</i> on the root of the scope</li>
</ul>
<textarea v-model="rules"></textarea>
<hr>
<h2>CSS</h2>
<p class="small">Costum user CSS</p>
<textarea name="css"></textarea>
</div>
</template>
<script>
import api from '@/utils/api'
export default {
name: 'user',
data: () => {
return {
admin: false,
allowNew: false,
allowEdit: false,
allowCommands: false,
passwordBlock: true,
password: '',
username: '',
scope: '',
rules: '',
css: '',
commands: ''
}
},
created () {
if (this.$route.path === '/users/new') return
api.getUser(this.$route.params[0]).then(user => {
this.admin = user.admin
this.allowCommands = user.allowCommands
this.allowNew = user.allowNew
this.allowEdit = user.allowEdit
this.scope = user.filesystem
this.username = user.username
this.commands = user.commands.join(' ')
this.css = user.css
for (let rule of user.rules) {
if (rule.allow) {
this.rules += 'allow '
} else {
this.rules += 'disallow '
}
if (rule.regex) {
this.rules += 'regex ' + rule.regexp.raw
} else {
this.rules += rule.path
}
this.rules += '\n'
}
}).catch(error => {
console.log(error)
})
},
watch: {
admin: function () {
if (!this.admin) return
this.allowCommands = true
this.allowEdit = true
this.allowNew = true
}
}
}
</script>
<style>
.dashboard {
max-width: 600px;
}
.dashboard textarea,
.dashboard input[type="text"],
.dashboard input[type="password"] {
padding: .5em 1em;
display: block;
border: 1px solid #e9e9e9;
transition: .2s ease border;
color: #333;
width: 100%;
}
.dashboard textarea:focus,
.dashboard textarea:hover,
.dashboard input[type="text"]:focus,
.dashboard input[type="password"]:focus,
.dashboard input[type="text"]:hover,
.dashboard input[type="password"]:hover {
border-color: #9f9f9f;
}
.dashboard textarea {
font-family: monospace;
min-height: 10em;
resize: vertical;
}
.dashboard p label {
margin-bottom: .2em;
display: block;
font-size: .8em
}
hr {
border-bottom: 2px solid rgba(181, 181, 181, 0.5);
border-top: 0;
border-right: 0;
border-left: 0;
margin: 1em 0;
}
li code,
p code {
background: rgba(0, 0, 0, 0.05);
padding: .1em;
border-radius: .2em;
}
.small {
font-size: .8em;
line-height: 1.5;
}
</style>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Users</h1>
</div>
</template>
<script>
export default {
name: 'users'
}
</script>

View File

@ -3,6 +3,7 @@ body {
padding-top: 4em;
background-color: #f8f8f8;
user-select: none;
color: #333;
}
* {

View File

@ -2,6 +2,10 @@ import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login'
import Main from '@/components/Main'
import Files from '@/components/Files'
import Users from '@/components/Users'
import User from '@/components/User'
import Settings from '@/components/Settings'
import auth from '@/utils/auth.js'
Vue.use(Router)
@ -40,11 +44,29 @@ const router = new Router({
children: [
{
path: '/files/*',
name: 'Files'
name: 'Files',
component: Files
},
{
path: '/dashboard',
name: 'Dashboard'
path: '/settings',
name: 'Settings',
component: Settings
},
{
path: '/users',
name: 'Users',
component: Users
},
{
path: '/users/',
redirect: {
path: '/users'
}
},
{
path: '/users/*',
name: 'User',
component: User
},
{
path: '/*',

View File

@ -10,6 +10,7 @@ const state = {
req: {},
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
jwt: '',
loading: false,
reload: false,
selected: [],
multiple: false,

View File

@ -16,6 +16,7 @@ const mutations = {
state.show = 'error'
state.showMessage = value
},
setLoading: (state, value) => { state.loading = value },
setReload: (state, value) => { state.reload = value },
setUser: (state, value) => (state.user = value),
setJWT: (state, value) => (state.jwt = value),

View File

@ -105,7 +105,7 @@ function move (oldLink, newLink) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/resource${oldLink}`, true)
request.open('PATCH', `${store.state.baseURL}/api/resource${oldLink}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.setRequestHeader('Destination', newLink)
@ -190,6 +190,27 @@ function download (format, ...files) {
window.open(url)
}
function getUser (id) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve(JSON.parse(request.responseText))
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
export default {
delete: rm,
fetch,
@ -199,5 +220,6 @@ export default {
post,
command,
search,
download
download,
getUser
}

View File

@ -85,21 +85,21 @@ type User struct {
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string
Raw string `json:"raw"`
regexp *regexp.Regexp
}