UI Foundation
Added foundational Vue application for UI. The application is embedded and has foundational level functionality for setup, user authentication, bookmarks, applications, and categories.
This commit is contained in:
parent
6c9ea1f774
commit
2156529b1c
|
@ -1,2 +1,4 @@
|
|||
/target
|
||||
data.db*
|
||||
node_modules/
|
||||
dist/
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,7 +19,8 @@ actix-web = "4.0.0-rc.1"
|
|||
actix-rt = "2.6.0"
|
||||
tracing-test = "0.2.1"
|
||||
tracing-actix-web = "0.5.0-rc.1"
|
||||
cargo-embed = "0.12.0"
|
||||
rust-embed= { version="5.9.0", features = ["actix"] }
|
||||
mime_guess = "2.0.3"
|
||||
bcrypt = "0.10.1"
|
||||
actix-web-httpauth = "0.6.0-beta.7"
|
||||
jsonwebtoken = "8.0.1"
|
||||
|
|
|
@ -16,7 +16,3 @@ args = ["migrate", "run"]
|
|||
|
||||
[tasks.resetdb]
|
||||
run_task = { name = ["dropdb", "createdb", "migratedb"] }
|
||||
|
||||
[tasks.entity]
|
||||
command = "sea-orm-cli"
|
||||
args = [ "generate", "entity", "-o", "src/entity", "--with-serde", "both" ]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
Command::new("npm")
|
||||
.args(&["run", "build-dev"])
|
||||
.current_dir("./vade-ui")
|
||||
.status()
|
||||
.unwrap();
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": false,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "es2020",
|
||||
"module": "es2015"
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"build",
|
||||
".vscode",
|
||||
".nuxt",
|
||||
"coverage",
|
||||
"jspm_packages",
|
||||
"tmp",
|
||||
"temp",
|
||||
"bower_components",
|
||||
".npm",
|
||||
".yarn"
|
||||
],
|
||||
"typeAcquisition": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ CREATE TABLE bookmark (
|
|||
bookmark_name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT 1,
|
||||
bookmark_category_id INTEGER
|
||||
);
|
||||
CREATE TABLE bookmark_category (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
|
@ -26,7 +27,8 @@ CREATE TABLE bookmark_category (
|
|||
active BOOLEAN NOT NULL DEFAULT 1,
|
||||
glyph TEXT
|
||||
);
|
||||
CREATE TABLE settings (
|
||||
CREATE TABLE setting (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
setting_name TEXT NOT NULL,
|
||||
setting_value TEXT NOT NULL
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "vade-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "ui.js",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"build-dev": "vue-cli-service build --mode development"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-5",
|
||||
"axios": "^0.25.0",
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^3.0.0",
|
||||
"vue-icon-picker": "^1.0.0",
|
||||
"vue-router": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<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="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Vade Mecum - Dashboard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;900&display=swap" rel="stylesheet">
|
||||
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
|
@ -76,8 +76,25 @@ pub async fn delete_application_category(
|
|||
state: web::Data<AppState>,
|
||||
id: web::Path<i32>,
|
||||
) -> Result<HttpResponse> {
|
||||
let cat_id = id.into_inner();
|
||||
let recs: Vec<application::Model> = Application::find()
|
||||
.filter(application::Column::ApplicationCategoryId.eq(cat_id))
|
||||
.all(&state.db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
for rec in recs {
|
||||
application::ActiveModel {
|
||||
id: Set(rec.id),
|
||||
application_category_id: Set(None),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&state.db)
|
||||
.await?;
|
||||
}
|
||||
|
||||
ApplicationCategory::delete_many()
|
||||
.filter(application_category::Column::Id.eq(id.into_inner()))
|
||||
.filter(application_category::Column::Id.eq(cat_id))
|
||||
.exec(&state.db)
|
||||
.await?;
|
||||
|
||||
|
@ -135,7 +152,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/application_categories")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/api/application_categories")
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -160,7 +177,7 @@ mod tests {
|
|||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/application_categories/{}",
|
||||
"/api/application_categories/{}",
|
||||
model.id
|
||||
))
|
||||
.method(Method::GET)
|
||||
|
@ -185,7 +202,7 @@ mod tests {
|
|||
|
||||
let state = setup_state().await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/application_categories")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/api/application_categories")
|
||||
.method(Method::POST)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -225,7 +242,7 @@ mod tests {
|
|||
model.category_name = "Another name".into();
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/application_categories/{}",
|
||||
"/api/application_categories/{}",
|
||||
model.id
|
||||
))
|
||||
.method(Method::PUT)
|
||||
|
@ -258,8 +275,18 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let app = application::ActiveModel {
|
||||
app_name: Set("Application 1".into()),
|
||||
url: Set("http://somewhere/".into()),
|
||||
active: Set(true),
|
||||
application_category_id: Set(Some(model.id)),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/application_categories/{}",
|
||||
"/api/application_categories/{}",
|
||||
model.id
|
||||
))
|
||||
.method(Method::DELETE)
|
||||
|
@ -275,6 +302,13 @@ mod tests {
|
|||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
let app = application::Entity::find_by_id(app.id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(app.application_category_id, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -316,7 +350,7 @@ mod tests {
|
|||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/application_categories/{}/applications",
|
||||
"/api/application_categories/{}/applications",
|
||||
category.id
|
||||
))
|
||||
.method(Method::GET)
|
||||
|
|
|
@ -123,7 +123,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/applications")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/api/applications")
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -146,7 +146,7 @@ mod tests {
|
|||
.await?;
|
||||
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||
actix_web::test::TestRequest::with_uri(&format!("/api/applications/{}", model.id))
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -172,7 +172,7 @@ mod tests {
|
|||
|
||||
let state = setup_state().await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/applications")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/api/applications")
|
||||
.method(Method::POST)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -209,7 +209,7 @@ mod tests {
|
|||
model.url = "http://updated.com".into();
|
||||
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||
actix_web::test::TestRequest::with_uri(&format!("/api/applications/{}", model.id))
|
||||
.method(Method::PUT)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -243,7 +243,7 @@ mod tests {
|
|||
.await?;
|
||||
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||
actix_web::test::TestRequest::with_uri(&format!("/api/applications/{}", model.id))
|
||||
.method(Method::DELETE)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
|
|
@ -4,7 +4,7 @@ use jsonwebtoken::{encode, EncodingKey, Header};
|
|||
use tracing::instrument;
|
||||
|
||||
use crate::api::api_prelude::*;
|
||||
use crate::auth::{get_secret, set_password, verify_password};
|
||||
use crate::auth::{generate_secret, get_secret, set_password, verify_password};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
@ -76,6 +76,7 @@ pub async fn initial_setup(
|
|||
.await?;
|
||||
if count == 0 {
|
||||
set_password(&state.db, &req.password).await?;
|
||||
generate_secret(&state.db).await?;
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Err(Error::new(
|
||||
|
@ -109,7 +110,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/authorize")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/api/authorize")
|
||||
.method(Method::POST)
|
||||
.set_json(super::AuthRequest {
|
||||
password: test_password,
|
||||
|
|
|
@ -135,7 +135,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/bookmark_categories")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/api/bookmark_categories")
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -156,8 +156,10 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id))
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/api/bookmark_categories/{}",
|
||||
model.id
|
||||
))
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -180,7 +182,7 @@ mod tests {
|
|||
|
||||
let state = setup_state().await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/bookmark_categories")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/api/bookmark_categories")
|
||||
.method(Method::POST)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -214,8 +216,10 @@ mod tests {
|
|||
|
||||
model.category_name = "Another name".into();
|
||||
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id))
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/api/bookmark_categories/{}",
|
||||
model.id
|
||||
))
|
||||
.method(Method::PUT)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -246,8 +250,10 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id))
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/api/bookmark_categories/{}",
|
||||
model.id
|
||||
))
|
||||
.method(Method::DELETE)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -294,7 +300,7 @@ mod tests {
|
|||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/bookmark_categories/{}/bookmarks",
|
||||
"/api/bookmark_categories/{}/bookmarks",
|
||||
category.id
|
||||
))
|
||||
.method(Method::GET)
|
||||
|
|
|
@ -116,7 +116,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/bookmarks")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/api/bookmarks")
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -138,7 +138,8 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id))
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/api/bookmarks/{}", model.id))
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -162,7 +163,7 @@ mod tests {
|
|||
|
||||
let state = setup_state().await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/bookmarks")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/api/bookmarks")
|
||||
.method(Method::POST)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -198,7 +199,8 @@ mod tests {
|
|||
|
||||
model.url = "http://updated.com".into();
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id))
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/api/bookmarks/{}", model.id))
|
||||
.method(Method::PUT)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -230,7 +232,8 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id))
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/api/bookmarks/{}", model.id))
|
||||
.method(Method::DELETE)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
|
|
@ -148,7 +148,7 @@ pub fn routes() -> Scope {
|
|||
.service(api::bookmark_categories::routes())
|
||||
.service(api::authorization::update_password);
|
||||
|
||||
Scope::new("")
|
||||
Scope::new("api")
|
||||
.service(api::authorization::authorize)
|
||||
.service(api::authorization::initial_setup)
|
||||
.service(protected_routes)
|
||||
|
|
|
@ -112,7 +112,7 @@ pub async fn get_secret(db: &DatabaseConnection) -> crate::error::Result<Vec<u8>
|
|||
Err(Error::new(ErrorCode::Internal, "Could not decode secret"))
|
||||
}
|
||||
} else {
|
||||
Err(Error::new(ErrorCode::Internal, "No secret found"))
|
||||
Err(Error::new(ErrorCode::NoSetup, "Not setup"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.5.0
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
12
src/error.rs
12
src/error.rs
|
@ -1,3 +1,4 @@
|
|||
use actix_web::http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
@ -8,6 +9,7 @@ pub enum ErrorCode {
|
|||
DatabaseError,
|
||||
UnAuthorized,
|
||||
Internal,
|
||||
NoSetup,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
|
@ -47,7 +49,15 @@ impl From<&str> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl actix_web::error::ResponseError for Error {}
|
||||
impl actix_web::error::ResponseError for Error {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
match self.code {
|
||||
ErrorCode::UnAuthorized => StatusCode::UNAUTHORIZED,
|
||||
ErrorCode::NoSetup => StatusCode::UPGRADE_REQUIRED,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sea_orm::DbErr> for Error {
|
||||
fn from(e: sea_orm::DbErr) -> Self {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { createApp } from 'vue'
|
||||
import App from './ui/App.vue'
|
||||
import router from './ui/router'
|
||||
import axios from "axios"
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
|
||||
axios.interceptors.request.use(function (config) {
|
||||
const token = localStorage.getItem("token");
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(function (response) {
|
||||
return response;
|
||||
}, function (error) {
|
||||
console.log(error);
|
||||
if (error.response.status === 426) {
|
||||
router.push("/setup");
|
||||
}
|
||||
if (error.response.status === 401) {
|
||||
router.push("/login");
|
||||
}
|
||||
return error;
|
||||
});
|
||||
|
||||
|
||||
library.add(fas)
|
||||
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.component("font-awesome-icon", FontAwesomeIcon)
|
||||
.mount('#app')
|
31
src/main.rs
31
src/main.rs
|
@ -1,4 +1,5 @@
|
|||
use actix_web::{web, App, HttpServer};
|
||||
use actix_web::{get, web, App, HttpResponse, HttpServer};
|
||||
use rust_embed::RustEmbed;
|
||||
use sea_orm::{Database, DatabaseConnection};
|
||||
use tracing::{info, instrument};
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
@ -29,7 +30,12 @@ async fn main() {
|
|||
|
||||
info!("Starting http server on 8080");
|
||||
|
||||
HttpServer::new(move || App::new().app_data(state.clone()).service(api::routes()))
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(state.clone())
|
||||
.service(api::routes())
|
||||
.service(dist)
|
||||
})
|
||||
.bind("127.0.0.1:8080")
|
||||
.unwrap()
|
||||
.run()
|
||||
|
@ -37,6 +43,27 @@ async fn main() {
|
|||
.expect("Couldnt launch server");
|
||||
}
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "dist"]
|
||||
struct UIAssets;
|
||||
|
||||
#[get("/{filename:.*}")]
|
||||
async fn dist(path: web::Path<String>) -> HttpResponse {
|
||||
let path = if UIAssets::get(&*path).is_some() {
|
||||
&*path
|
||||
} else {
|
||||
"index.html"
|
||||
};
|
||||
let content = UIAssets::get(path).unwrap();
|
||||
let body: actix_web::body::BoxBody = match content {
|
||||
std::borrow::Cow::Borrowed(bytes) => actix_web::body::BoxBody::new(bytes),
|
||||
std::borrow::Cow::Owned(bytes) => actix_web::body::BoxBody::new(bytes),
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
.content_type(mime_guess::from_path(path).first_or_octet_stream().as_ref())
|
||||
.body(body)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn setup_database() -> Result<DatabaseConnection, sea_orm::DbErr> {
|
||||
Database::connect("sqlite://data.db").await
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<div>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 35px;
|
||||
background-color: #222;
|
||||
background-repeat: none;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
a,
|
||||
a:hover,
|
||||
a:active,
|
||||
label {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#app {
|
||||
color: #fff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
* {
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<modal :open="open">
|
||||
<template #body>
|
||||
<form @submit.prevent="save">
|
||||
<text-field v-model="category.categoryName" label="Name" />
|
||||
<icon-picker v-model="category.glyph" />
|
||||
</form>
|
||||
</template>
|
||||
<template #actions>
|
||||
<btn @click="delCategory" label="Delete" danger v-if="!!this.category.id" />
|
||||
<btn @click="close" label="Cancel" />
|
||||
<btn @click="submit" :label="saveLabel" />
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
<script>
|
||||
import TextField from "./TextField.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import IconPicker from "./IconPicker.vue";
|
||||
import axios from "axios";
|
||||
import Btn from "./Button.vue";
|
||||
|
||||
export default {
|
||||
components: { TextField, Modal, IconPicker, Btn },
|
||||
props: ["open", "mode", "data"],
|
||||
watch: {
|
||||
data: function (next) {
|
||||
if (next) {
|
||||
this.category = JSON.parse(JSON.stringify(next));
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
category: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
saveLabel() {
|
||||
return this.category.id ? "Update" : "Save";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
async delCategory() {
|
||||
let resp = await axios.delete(
|
||||
`/api/application_categories/${this.category.id}`,
|
||||
this.category
|
||||
);
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
},
|
||||
async submit() {
|
||||
if (this.category.id) {
|
||||
let resp = await axios.put(
|
||||
`/api/application_categories/${this.category.id}`,
|
||||
this.category
|
||||
);
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
} else {
|
||||
let resp = await axios.post("/api/application_categories", {
|
||||
active: true,
|
||||
...this.category,
|
||||
});
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,90 @@
|
|||
|
||||
<template>
|
||||
<modal :open="open">
|
||||
<template #body>
|
||||
<form @submit.prevent="">
|
||||
<select-field
|
||||
v-model="app.applicationCategoryId"
|
||||
label="Category"
|
||||
:items="selectCategories"
|
||||
emptyLabel="No Category"
|
||||
/>
|
||||
<text-field v-model="app.appName" label="Name" />
|
||||
<text-field v-model="app.description" label="Description" />
|
||||
<text-field v-model="app.url" label="URL" />
|
||||
<icon-picker v-model="app.glyph" />
|
||||
</form>
|
||||
</template>
|
||||
<template #actions>
|
||||
<btn @click="delApp" label="Delete" danger v-if="!!this.app.id" />
|
||||
<btn @click="close" label="Cancel" />
|
||||
<btn :label="saveLabel" type="button" @click="save" />
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
<script>
|
||||
import TextField from "./TextField.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import IconPicker from "./IconPicker.vue";
|
||||
import axios from "axios";
|
||||
import Btn from "./Button.vue";
|
||||
import SelectField from "./SelectField.vue";
|
||||
|
||||
export default {
|
||||
components: { TextField, Modal, IconPicker, Btn, SelectField },
|
||||
props: ["open", "mode", "data", "categories"],
|
||||
watch: {
|
||||
data: function (next) {
|
||||
if (next) {
|
||||
this.app = JSON.parse(JSON.stringify(next));
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
app: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
saveLabel() {
|
||||
return this.app.id ? "Update" : "Save";
|
||||
},
|
||||
selectCategories() {
|
||||
return this.categories.map((i) => ({ label: i.categoryName, value: i.id }));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
async delApp() {
|
||||
let resp = await axios.delete(
|
||||
`/api/applications/${this.app.id}`,
|
||||
this.app
|
||||
);
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
},
|
||||
async save() {
|
||||
if (this.app.id) {
|
||||
let resp = await axios.put(
|
||||
`/api/applications/${this.app.id}`,
|
||||
this.app
|
||||
);
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
} else {
|
||||
let resp = await axios.post("/api/applications", {
|
||||
active: true,
|
||||
...this.app,
|
||||
});
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="tile" @click="click">
|
||||
<font-awesome-icon :icon="appData.glyph" size="2x" />
|
||||
<div class="label">
|
||||
<div class="title">{{appData.appName}}</div>
|
||||
<div class="description">{{appData.description}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["appData"],
|
||||
methods: {
|
||||
click(e) {
|
||||
this.$emit("clicked", e, this.appData);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.tile {
|
||||
transition: all 0.5s;
|
||||
padding: 16px 25px;
|
||||
border-radius: 5px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
background: transparent;
|
||||
}
|
||||
.tile:hover {
|
||||
transition: all 0.1s;
|
||||
background-color: #333;
|
||||
box-shadow: rgba(0, 0, 0, 0.5) 0px 20px 30px -10px;
|
||||
}
|
||||
.label {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
svg {
|
||||
color: #fff;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.title {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
}
|
||||
.description {
|
||||
color: #ccc;
|
||||
font-size: 0.8em;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<modal :open="open">
|
||||
<template #body>
|
||||
<form @submit.prevent="save">
|
||||
<text-field v-model="category.categoryName" label="Name" />
|
||||
<icon-picker v-model="category.glyph" />
|
||||
</form>
|
||||
</template>
|
||||
<template #actions>
|
||||
<btn @click="delCategory" label="Delete" danger v-if="!!this.category.id" />
|
||||
<btn @click="close" label="Cancel" />
|
||||
<btn @click="submit" :label="saveLabel" />
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
<script>
|
||||
import TextField from "./TextField.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import IconPicker from "./IconPicker.vue";
|
||||
import axios from "axios";
|
||||
import Btn from "./Button.vue";
|
||||
|
||||
export default {
|
||||
components: { TextField, Modal, IconPicker, Btn },
|
||||
props: ["open", "mode", "data"],
|
||||
watch: {
|
||||
data: function (next) {
|
||||
if (next) {
|
||||
this.category = JSON.parse(JSON.stringify(next));
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
category: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
saveLabel() {
|
||||
return this.category.id ? "Update" : "Save";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
async delCategory() {
|
||||
let resp = await axios.delete(
|
||||
`/api/bookmark_categories/${this.category.id}`,
|
||||
this.category
|
||||
);
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
},
|
||||
async submit() {
|
||||
if (this.category.id) {
|
||||
let resp = await axios.put(
|
||||
`/api/bookmark_categories/${this.category.id}`,
|
||||
this.category
|
||||
);
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
} else {
|
||||
let resp = await axios.post("/api/bookmark_categories", {
|
||||
active: true,
|
||||
...this.category,
|
||||
});
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<modal :open="open">
|
||||
<template #body>
|
||||
<form @submit.prevent="">
|
||||
<select-field
|
||||
v-model="bookmark.bookmarkCategoryId"
|
||||
label="Category"
|
||||
:items="selectCategories"
|
||||
emptyLabel="No Category"
|
||||
/>
|
||||
<text-field v-model="bookmark.bookmarkName" label="Name" />
|
||||
<text-field v-model="bookmark.url" label="URL" />
|
||||
</form>
|
||||
</template>
|
||||
<template #actions>
|
||||
<btn @click="delBookmark" label="Delete" danger v-if="!!this.bookmark.id" />
|
||||
<btn @click="close" label="Cancel" />
|
||||
<btn :label="saveLabel" type="button" @click="save" />
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
<script>
|
||||
import TextField from "./TextField.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import axios from "axios";
|
||||
import Btn from "./Button.vue";
|
||||
import SelectField from "./SelectField.vue";
|
||||
|
||||
export default {
|
||||
components: { TextField, Modal, Btn, SelectField },
|
||||
props: ["open", "mode", "data", "categories"],
|
||||
watch: {
|
||||
data: function (next) {
|
||||
if (next) {
|
||||
this.bookmark = JSON.parse(JSON.stringify(next));
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bookmark: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
saveLabel() {
|
||||
return this.bookmark.id ? "Update" : "Save";
|
||||
},
|
||||
selectCategories() {
|
||||
return this.categories.map((i) => ({ label: i.categoryName, value: i.id }));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
async delBookmark() {
|
||||
let resp = await axios.delete(
|
||||
`/api/bookmarks/${this.bookmark.id}`,
|
||||
this.bookmark
|
||||
);
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
},
|
||||
async save() {
|
||||
if (this.bookmark.id) {
|
||||
let resp = await axios.put(
|
||||
`/api/bookmarks/${this.bookmark.id}`,
|
||||
this.bookmark
|
||||
);
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
} else {
|
||||
let resp = await axios.post("/api/bookmarks", {
|
||||
active: true,
|
||||
...this.bookmark,
|
||||
});
|
||||
if (resp.status == 200) {
|
||||
this.$emit("update");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div class="bookmark-tile" @click="click">
|
||||
<font-awesome-icon icon="external-link-alt"/>
|
||||
{{bookmark.bookmarkName}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["bookmark"],
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit("clicked", this.bookmark);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bookmark-tile {
|
||||
padding: 5px 25px;
|
||||
}
|
||||
|
||||
.bookmark-tile, .bookmark-tile svg {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.bookmark-tile:hover, .bookmark-tile:hover svg {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<button :class="{danger: danger}">{{label}}</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: String,
|
||||
danger: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
padding: 10px 18px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
border: 2px solid #222;
|
||||
background: #333;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
color: #cc0000;
|
||||
background-color: #200;
|
||||
border: 2px solid #200;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transition: all 0.2s;
|
||||
background: #444;
|
||||
}
|
||||
|
||||
button.danger:hover {
|
||||
background: #400;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div :class="{btn: true, active: editMode}">
|
||||
<font-awesome-icon icon="cogs" size="2x" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["editMode"],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.btn {
|
||||
transition: all 0.2s;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 40px;
|
||||
text-align: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.btn svg {
|
||||
margin: auto auto;
|
||||
color: #777;
|
||||
}
|
||||
.btn:hover {
|
||||
transition: all 0.2s;
|
||||
opacity: 1;
|
||||
}
|
||||
.btn:hover svg {
|
||||
color: #ccc;
|
||||
}
|
||||
.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.btn.active svg {
|
||||
color: #8844cc;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<div class="form-field">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style>
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
text-align: left;
|
||||
}
|
||||
label {
|
||||
margin-right: 15px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
width: 150px;
|
||||
}
|
||||
.form-field + .form-field {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.form-field + button {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div v-bind:class="{modal: true, open: open}">
|
||||
<div class="content">
|
||||
<div class="body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ["open"],
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
@keyframes open {
|
||||
0% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0;
|
||||
}
|
||||
25% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 100; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0, 0, 0); /* Fallback color */
|
||||
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
|
||||
}
|
||||
.modal-backdrop .open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: transparent;
|
||||
position: fixed; /* Stay in place */
|
||||
top: 5%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0%);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal.open .content {
|
||||
width: 600px;
|
||||
margin: auto auto;
|
||||
animation: open 0.5s;
|
||||
animation-timing-function: cubic-bezier(0.455, 0.03, 0.515, 0.955);
|
||||
}
|
||||
|
||||
.modal .content {
|
||||
transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1), 0 6px 6px rgba(0, 0, 0, 0.16);
|
||||
|
||||
background: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
max-width: 80%;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.modal .content .body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.modal .actions {
|
||||
background: #3c3c3c;
|
||||
border-top: 1px solid #2c2c2c;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: 15px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.modal .actions button + button {
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="new tile">
|
||||
<font-awesome-icon icon="plus" />
|
||||
<div class="label">
|
||||
<div class="title">{{title}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["title"],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tile.new {
|
||||
background: #444;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="panel">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
border-radius: 5px;
|
||||
background: #333;
|
||||
padding: 25px;
|
||||
box-shadow: rgb(0, 0, 0) 0px 20px 30px -10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Panel",
|
||||
setup() {},
|
||||
};
|
||||
</script>>
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<form-field>
|
||||
<label>{{label}}</label>
|
||||
<select @change="handleChange">
|
||||
<option :selected="!modelValue" v-if="emptyLabel" value>{{emptyLabel}}</option>
|
||||
<option
|
||||
:selected="modelValue === item.value"
|
||||
v-for="item in items"
|
||||
:key="item"
|
||||
:value="item.value"
|
||||
>{{item.label}}</option>
|
||||
</select>
|
||||
</form-field>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormField from "./FormField.vue";
|
||||
export default {
|
||||
components: { FormField },
|
||||
props: ["items", "modelValue", "label", "emptyLabel"],
|
||||
methods: {
|
||||
handleChange(e) {
|
||||
this.$emit("update:modelValue", parseInt(e.target.value));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
select {
|
||||
flex: 1;
|
||||
display: block;
|
||||
color: #ccc;
|
||||
line-height: 1.3;
|
||||
padding: 0.6em 1.4em 0.5em 0.8em;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
border: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: #121212;
|
||||
}
|
||||
select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
select:hover {
|
||||
border-color: #888;
|
||||
}
|
||||
select:focus {
|
||||
color: #ccc;
|
||||
outline: none;
|
||||
}
|
||||
select option {
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<form-field>
|
||||
<label v-if="label">{{label}}</label>
|
||||
<input :value="modelValue" ref="field" @input="handleInput" :type="inputType" />
|
||||
</form-field>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormField from "./FormField.vue";
|
||||
export default {
|
||||
props: {
|
||||
label: String,
|
||||
modelValue: String,
|
||||
password: Boolean,
|
||||
},
|
||||
computed: {
|
||||
inputType() {
|
||||
return this.password ? "password" : "text";
|
||||
},
|
||||
},
|
||||
components: { FormField },
|
||||
methods: {
|
||||
handleInput() {
|
||||
this.$emit("update:modelValue", this.$refs.field.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
color: #fff;
|
||||
background-color: #121212;
|
||||
outline: none;
|
||||
border: 1px solid #000;
|
||||
flex: 1;
|
||||
padding: 8px 15px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Dashboard from "../views/Dashboard.vue"
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/logout',
|
||||
name: 'Logout',
|
||||
component: () => import(/* webpackChunkName: "login" */ '../views/Logout.vue')
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/setup',
|
||||
name: 'Setup',
|
||||
component: () => import(/* webpackChunkName: "login" */ '../views/Setup.vue')
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: Dashboard,
|
||||
},
|
||||
{
|
||||
path: '/admin/applications/new',
|
||||
name: 'NewApplication',
|
||||
component: () => import(/* webpackChunkName: "login" */ '../views/CreateApplication.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<form @submit.prevent="save">
|
||||
<panel>
|
||||
<text-field v-model="appName" label="Name" />
|
||||
<text-field v-model="description" label="Description" />
|
||||
<text-field v-model="url" label="URL" />
|
||||
<text-field v-model="glyph" label="Glyph" />
|
||||
<btn @click="save" label="Save" />
|
||||
</panel>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Panel from "../components/Panel.vue";
|
||||
import TextField from "../components/TextField.vue";
|
||||
import Btn from "../components/Button.vue";
|
||||
import axios from "axios";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
appName: "app name",
|
||||
description: "test description",
|
||||
url: "http://example.com",
|
||||
glyph: "information",
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Panel,
|
||||
TextField,
|
||||
Btn,
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
axios.post("/api/applications", {
|
||||
appName: this.appName,
|
||||
description: this.description,
|
||||
glyph: this.glyph,
|
||||
url: this.url,
|
||||
active: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 600px;
|
||||
margin: auto auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,267 @@
|
|||
<template>
|
||||
<div class="dashboard">
|
||||
<div class="editmode-tiles">
|
||||
<new-item-tile title="New Application" v-if="editMode" @click="openNewApp" />
|
||||
<new-item-tile title="New App Category" v-if="editMode" @click="openNewAppCat" />
|
||||
<new-item-tile title="New Bookmark" v-if="editMode" @click="openNewBookmark" />
|
||||
<new-item-tile title="New Bookmark Category" v-if="editMode" @click="openNewBookmarkCat" />
|
||||
</div>
|
||||
<div class="container" v-if="appsForCategory(null).length > 0 || editMode">
|
||||
<h1>APPLICATIONS</h1>
|
||||
<div class="app-tiles">
|
||||
<application-tile
|
||||
@clicked="appTileClicked"
|
||||
v-for="app in appsForCategory(null)"
|
||||
:appData="app"
|
||||
:key="app.id"
|
||||
/>
|
||||
<application-modal
|
||||
:open="appOpen"
|
||||
:data="editApp"
|
||||
:categories="applicationCategories"
|
||||
@close="closeNewApp"
|
||||
@update="reload"
|
||||
/>
|
||||
<application-category-modal
|
||||
:open="appCatOpen"
|
||||
:data="editAppCat"
|
||||
@close="closeNewAppCat"
|
||||
@update="reload"
|
||||
/>
|
||||
<bookmark-modal
|
||||
:open="bookmarkOpen"
|
||||
:data="editBookmark"
|
||||
:categories="bookmarkCategories"
|
||||
@close="closeBookmark"
|
||||
@update="reload"
|
||||
/>
|
||||
<bookmark-category-modal
|
||||
:open="bookmarkCatOpen"
|
||||
:data="editBookmarkCat"
|
||||
@close="closeNewBookmarkCat"
|
||||
@update="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" v-for="cat in applicationCategories" :key="cat.id">
|
||||
<h1 @click="appCatClicked(cat)">
|
||||
<font-awesome-icon v-if="cat.glyph" :icon="cat.glyph" />
|
||||
{{cat.categoryName}}
|
||||
</h1>
|
||||
<div class="app-tiles">
|
||||
<application-tile
|
||||
@clicked="appTileClicked"
|
||||
v-for="app in appsForCategory(cat.id)"
|
||||
:appData="app"
|
||||
:key="app.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1 class="bookmark">BOOKMARKS</h1>
|
||||
<div class="bookmark-container">
|
||||
<div class="bookmark-category">
|
||||
<h2>UNCATEGORZED</h2>
|
||||
<bookmark-tile
|
||||
v-for="bm in bookmarksForCategory(null)"
|
||||
:key="bm.id"
|
||||
:bookmark="bm"
|
||||
@clicked="bookmarkClicked"
|
||||
/>
|
||||
</div>
|
||||
<div class="bookmark-category" v-for="cat in bookmarkCategories" :key="cat.id">
|
||||
<h2 @click="bookmarkCatClicked(cat)">
|
||||
<font-awesome-icon v-if="cat.glyph" :icon="cat.glyph" />
|
||||
{{cat.categoryName}}
|
||||
</h2>
|
||||
<bookmark-tile
|
||||
@clicked="bookmarkClicked"
|
||||
v-for="bm in bookmarksForCategory(cat.id)"
|
||||
:key="bm.id"
|
||||
:bookmark="bm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edit-mode-button :editMode="editMode" @click="toggleEdit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import ApplicationTile from "../components/ApplicationTile.vue";
|
||||
import ApplicationModal from "../components/ApplicationModal.vue";
|
||||
import ApplicationCategoryModal from "../components/ApplicationCategoryModal.vue";
|
||||
import EditModeButton from "../components/EditModeButton.vue";
|
||||
import NewItemTile from "../components/NewItemTile.vue";
|
||||
import BookmarkModal from "../components/BookmarkModal.vue";
|
||||
import BookmarkTile from "../components/BookmarkTile.vue";
|
||||
import BookmarkCategoryModal from "../components/BookmarkCategoryModal.vue";
|
||||
|
||||
export default {
|
||||
name: "Dashboard",
|
||||
methods: {
|
||||
appsForCategory(catId) {
|
||||
return this.applications.filter((i) => i.applicationCategoryId === catId);
|
||||
},
|
||||
bookmarksForCategory(catId) {
|
||||
return this.bookmarks.filter((i) => i.bookmarkCategoryId === catId);
|
||||
},
|
||||
openNewApp() {
|
||||
this.editApp = {};
|
||||
this.appOpen = true;
|
||||
},
|
||||
openNewBookmark() {
|
||||
this.bookmarkOpen = true;
|
||||
},
|
||||
closeNewApp() {
|
||||
this.editApp = {};
|
||||
this.appOpen = false;
|
||||
},
|
||||
closeBookmark() {
|
||||
this.editBookmark = {};
|
||||
this.bookmarkOpen = false;
|
||||
},
|
||||
openNewAppCat() {
|
||||
this.appCatOpen = true;
|
||||
this.editAppCat = {};
|
||||
},
|
||||
closeNewAppCat() {
|
||||
this.editAppCat = {};
|
||||
this.appCatOpen = false;
|
||||
},
|
||||
openNewBookmarkCat() {
|
||||
this.bookmarkCatOpen = true;
|
||||
},
|
||||
closeNewBookmarkCat() {
|
||||
this.editBookmarkCat = {};
|
||||
this.bookmarkCatOpen = false;
|
||||
},
|
||||
toggleEdit() {
|
||||
this.editMode = !this.editMode;
|
||||
},
|
||||
appTileClicked(e, app) {
|
||||
if (this.editMode) {
|
||||
this.editApp = app;
|
||||
this.appOpen = true;
|
||||
} else {
|
||||
window.open(app.url);
|
||||
}
|
||||
},
|
||||
appCatClicked(cat) {
|
||||
if (this.editMode) {
|
||||
this.editAppCat = cat;
|
||||
this.appCatOpen = true;
|
||||
}
|
||||
},
|
||||
bookmarkCatClicked(cat) {
|
||||
if (this.editMode) {
|
||||
this.editBookmarkCat = cat;
|
||||
this.bookmarkCatOpen = true;
|
||||
}
|
||||
},
|
||||
bookmarkClicked(bm) {
|
||||
if (this.editMode) {
|
||||
this.editBookmark = bm;
|
||||
this.bookmarkOpen = true;
|
||||
} else {
|
||||
window.open(bm.url);
|
||||
}
|
||||
},
|
||||
async reload() {
|
||||
this.applications = (await axios.get("/api/applications")).data.items;
|
||||
this.applicationCategories = (
|
||||
await axios.get("/api/application_categories")
|
||||
).data.items;
|
||||
this.bookmarks = (await axios.get("/api/bookmarks")).data.items;
|
||||
this.bookmarkCategories = (
|
||||
await axios.get("/api/bookmark_categories")
|
||||
).data.items;
|
||||
this.editApp = {};
|
||||
this.newAppOpen = false;
|
||||
this.appCatOpen = false;
|
||||
this.bookmarkCatOpen = false;
|
||||
this.bookmarkOpen = false;
|
||||
this.editAppCat = {};
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ApplicationTile,
|
||||
ApplicationModal,
|
||||
ApplicationCategoryModal,
|
||||
EditModeButton,
|
||||
NewItemTile,
|
||||
BookmarkModal,
|
||||
BookmarkTile,
|
||||
BookmarkCategoryModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editMode: false,
|
||||
applications: [],
|
||||
applicationCategories: [],
|
||||
appOpen: false,
|
||||
appCatOpen: false,
|
||||
bookmarkOpen: false,
|
||||
bookmarkCatOpen: false,
|
||||
editApp: {},
|
||||
editAppCat: {},
|
||||
editBookmark: {},
|
||||
editBookmarkCat: {},
|
||||
bookmarks: [],
|
||||
bookmarkCategories: [],
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.reload();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scope>
|
||||
h1 {
|
||||
margin-top: 0px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
margin-bottom: 5px;
|
||||
padding-left: 25px;
|
||||
font-size: 24px;
|
||||
}
|
||||
h2 {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
padding-left: 25px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
h2 svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.app-tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
}
|
||||
.container {
|
||||
padding: 10px 50px;
|
||||
width: 1000px;
|
||||
margin: auto auto;
|
||||
}
|
||||
.dashboard {
|
||||
margin-top: 150px;
|
||||
}
|
||||
.editmode-tiles {
|
||||
display: flex;
|
||||
padding: 10px 50px;
|
||||
width: 1000px;
|
||||
margin: 15px auto;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.bookmark-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div class="login-wrapper">
|
||||
<form @submit.prevent="goLogin">
|
||||
<panel>
|
||||
<input v-model="password" type="password" autofocus />
|
||||
<button @clicked="goLogin" type="submit">Authenticate</button>
|
||||
</panel>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Panel from "../components/Panel.vue";
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
components: {
|
||||
Panel
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
password: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async goLogin() {
|
||||
const response = await axios.post("/api/authorize", {
|
||||
password: this.password,
|
||||
});
|
||||
localStorage.setItem("token", response.data.token);
|
||||
this.$router.push("/");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 100vh;
|
||||
align-items: center;
|
||||
justify-items: center; /* adjusted */
|
||||
}
|
||||
input {
|
||||
background: #333;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
background: #121212;
|
||||
font-weight: bold;
|
||||
padding: 12px 22px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #777;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
button {
|
||||
transition: all 0.2s;
|
||||
padding: 10px 18px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
border: 1px solid #000;
|
||||
background: #121212;
|
||||
box-shadow: none;
|
||||
}
|
||||
button:hover {
|
||||
transition: all 0.2s;
|
||||
box-shadow: rgba(0, 0, 0, 0.25) 0px 14px 28px,
|
||||
rgba(0, 0, 0, 0.22) 0px 10px 10px;
|
||||
}
|
||||
</style>>
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
created() {
|
||||
localStorage.removeItem("token");
|
||||
this.$router.push("/login");
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<panel>
|
||||
<h1>Setup a password</h1>
|
||||
<text-field v-model="password" password />
|
||||
<btn label="Setup" @click="setup" />
|
||||
</panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Panel from "../components/Panel.vue";
|
||||
import TextField from "../components/TextField.vue";
|
||||
import Btn from "../components/Button.vue";
|
||||
import axios from "axios";
|
||||
export default {
|
||||
components: { Panel, TextField, Btn },
|
||||
data() {
|
||||
return {
|
||||
password: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async setup() {
|
||||
await axios.post("/api/setup", { password: this.password });
|
||||
this.$router.push("/");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 10px 50px;
|
||||
width: 500px;
|
||||
margin: 50px auto;
|
||||
color: #fff;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
# vade-ui
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
Loading…
Reference in New Issue