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
|
/target
|
||||||
data.db*
|
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"
|
actix-rt = "2.6.0"
|
||||||
tracing-test = "0.2.1"
|
tracing-test = "0.2.1"
|
||||||
tracing-actix-web = "0.5.0-rc.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"
|
bcrypt = "0.10.1"
|
||||||
actix-web-httpauth = "0.6.0-beta.7"
|
actix-web-httpauth = "0.6.0-beta.7"
|
||||||
jsonwebtoken = "8.0.1"
|
jsonwebtoken = "8.0.1"
|
||||||
|
|
|
@ -16,7 +16,3 @@ args = ["migrate", "run"]
|
||||||
|
|
||||||
[tasks.resetdb]
|
[tasks.resetdb]
|
||||||
run_task = { name = ["dropdb", "createdb", "migratedb"] }
|
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,
|
bookmark_name TEXT NOT NULL,
|
||||||
url TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
active BOOLEAN NOT NULL DEFAULT 1,
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
bookmark_category_id INTEGER
|
||||||
);
|
);
|
||||||
CREATE TABLE bookmark_category (
|
CREATE TABLE bookmark_category (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
@ -26,7 +27,8 @@ CREATE TABLE bookmark_category (
|
||||||
active BOOLEAN NOT NULL DEFAULT 1,
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
glyph TEXT
|
glyph TEXT
|
||||||
);
|
);
|
||||||
CREATE TABLE settings (
|
CREATE TABLE setting (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
setting_name TEXT NOT NULL,
|
setting_name TEXT NOT NULL,
|
||||||
setting_value 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>,
|
state: web::Data<AppState>,
|
||||||
id: web::Path<i32>,
|
id: web::Path<i32>,
|
||||||
) -> Result<HttpResponse> {
|
) -> 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()
|
ApplicationCategory::delete_many()
|
||||||
.filter(application_category::Column::Id.eq(id.into_inner()))
|
.filter(application_category::Column::Id.eq(cat_id))
|
||||||
.exec(&state.db)
|
.exec(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -135,7 +152,7 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.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)
|
.method(Method::GET)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
@ -160,7 +177,7 @@ mod tests {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
"/application_categories/{}",
|
"/api/application_categories/{}",
|
||||||
model.id
|
model.id
|
||||||
))
|
))
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
|
@ -185,7 +202,7 @@ mod tests {
|
||||||
|
|
||||||
let state = setup_state().await?;
|
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)
|
.method(Method::POST)
|
||||||
.set_json(model.clone())
|
.set_json(model.clone())
|
||||||
.to_request();
|
.to_request();
|
||||||
|
@ -225,7 +242,7 @@ mod tests {
|
||||||
model.category_name = "Another name".into();
|
model.category_name = "Another name".into();
|
||||||
|
|
||||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
"/application_categories/{}",
|
"/api/application_categories/{}",
|
||||||
model.id
|
model.id
|
||||||
))
|
))
|
||||||
.method(Method::PUT)
|
.method(Method::PUT)
|
||||||
|
@ -258,8 +275,18 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.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!(
|
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
"/application_categories/{}",
|
"/api/application_categories/{}",
|
||||||
model.id
|
model.id
|
||||||
))
|
))
|
||||||
.method(Method::DELETE)
|
.method(Method::DELETE)
|
||||||
|
@ -275,6 +302,13 @@ mod tests {
|
||||||
.await?,
|
.await?,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let app = application::Entity::find_by_id(app.id)
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(app.application_category_id, None);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,7 +350,7 @@ mod tests {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
"/application_categories/{}/applications",
|
"/api/application_categories/{}/applications",
|
||||||
category.id
|
category.id
|
||||||
))
|
))
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
|
|
|
@ -123,7 +123,7 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.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)
|
.method(Method::GET)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
@ -146,7 +146,7 @@ mod tests {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut req =
|
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)
|
.method(Method::GET)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
@ -172,7 +172,7 @@ mod tests {
|
||||||
|
|
||||||
let state = setup_state().await?;
|
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)
|
.method(Method::POST)
|
||||||
.set_json(model.clone())
|
.set_json(model.clone())
|
||||||
.to_request();
|
.to_request();
|
||||||
|
@ -209,7 +209,7 @@ mod tests {
|
||||||
model.url = "http://updated.com".into();
|
model.url = "http://updated.com".into();
|
||||||
|
|
||||||
let mut req =
|
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)
|
.method(Method::PUT)
|
||||||
.set_json(model.clone())
|
.set_json(model.clone())
|
||||||
.to_request();
|
.to_request();
|
||||||
|
@ -243,7 +243,7 @@ mod tests {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut req =
|
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)
|
.method(Method::DELETE)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
|
|
@ -4,7 +4,7 @@ use jsonwebtoken::{encode, EncodingKey, Header};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::api::api_prelude::*;
|
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};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
@ -76,6 +76,7 @@ pub async fn initial_setup(
|
||||||
.await?;
|
.await?;
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
set_password(&state.db, &req.password).await?;
|
set_password(&state.db, &req.password).await?;
|
||||||
|
generate_secret(&state.db).await?;
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::Ok().body(""))
|
||||||
} else {
|
} else {
|
||||||
Err(Error::new(
|
Err(Error::new(
|
||||||
|
@ -109,7 +110,7 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.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)
|
.method(Method::POST)
|
||||||
.set_json(super::AuthRequest {
|
.set_json(super::AuthRequest {
|
||||||
password: test_password,
|
password: test_password,
|
||||||
|
|
|
@ -135,7 +135,7 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.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)
|
.method(Method::GET)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
@ -156,8 +156,10 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut req =
|
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id))
|
"/api/bookmark_categories/{}",
|
||||||
|
model.id
|
||||||
|
))
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
@ -180,7 +182,7 @@ mod tests {
|
||||||
|
|
||||||
let state = setup_state().await?;
|
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)
|
.method(Method::POST)
|
||||||
.set_json(model.clone())
|
.set_json(model.clone())
|
||||||
.to_request();
|
.to_request();
|
||||||
|
@ -214,8 +216,10 @@ mod tests {
|
||||||
|
|
||||||
model.category_name = "Another name".into();
|
model.category_name = "Another name".into();
|
||||||
|
|
||||||
let mut req =
|
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id))
|
"/api/bookmark_categories/{}",
|
||||||
|
model.id
|
||||||
|
))
|
||||||
.method(Method::PUT)
|
.method(Method::PUT)
|
||||||
.set_json(model.clone())
|
.set_json(model.clone())
|
||||||
.to_request();
|
.to_request();
|
||||||
|
@ -246,8 +250,10 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut req =
|
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id))
|
"/api/bookmark_categories/{}",
|
||||||
|
model.id
|
||||||
|
))
|
||||||
.method(Method::DELETE)
|
.method(Method::DELETE)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
@ -294,7 +300,7 @@ mod tests {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
"/bookmark_categories/{}/bookmarks",
|
"/api/bookmark_categories/{}/bookmarks",
|
||||||
category.id
|
category.id
|
||||||
))
|
))
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
|
|
|
@ -116,7 +116,7 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.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)
|
.method(Method::GET)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
@ -138,7 +138,8 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.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)
|
.method(Method::GET)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
@ -162,7 +163,7 @@ mod tests {
|
||||||
|
|
||||||
let state = setup_state().await?;
|
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)
|
.method(Method::POST)
|
||||||
.set_json(model.clone())
|
.set_json(model.clone())
|
||||||
.to_request();
|
.to_request();
|
||||||
|
@ -198,7 +199,8 @@ mod tests {
|
||||||
|
|
||||||
model.url = "http://updated.com".into();
|
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)
|
.method(Method::PUT)
|
||||||
.set_json(model.clone())
|
.set_json(model.clone())
|
||||||
.to_request();
|
.to_request();
|
||||||
|
@ -230,7 +232,8 @@ mod tests {
|
||||||
.insert(&state.db)
|
.insert(&state.db)
|
||||||
.await?;
|
.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)
|
.method(Method::DELETE)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, state);
|
let resp = call_endpoint!(req, state);
|
||||||
|
|
|
@ -148,7 +148,7 @@ pub fn routes() -> Scope {
|
||||||
.service(api::bookmark_categories::routes())
|
.service(api::bookmark_categories::routes())
|
||||||
.service(api::authorization::update_password);
|
.service(api::authorization::update_password);
|
||||||
|
|
||||||
Scope::new("")
|
Scope::new("api")
|
||||||
.service(api::authorization::authorize)
|
.service(api::authorization::authorize)
|
||||||
.service(api::authorization::initial_setup)
|
.service(api::authorization::initial_setup)
|
||||||
.service(protected_routes)
|
.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"))
|
Err(Error::new(ErrorCode::Internal, "Could not decode secret"))
|
||||||
}
|
}
|
||||||
} else {
|
} 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 sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
@ -8,6 +9,7 @@ pub enum ErrorCode {
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
UnAuthorized,
|
UnAuthorized,
|
||||||
Internal,
|
Internal,
|
||||||
|
NoSetup,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[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 {
|
impl From<sea_orm::DbErr> for Error {
|
||||||
fn from(e: sea_orm::DbErr) -> Self {
|
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 sea_orm::{Database, DatabaseConnection};
|
||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
|
@ -29,7 +30,12 @@ async fn main() {
|
||||||
|
|
||||||
info!("Starting http server on 8080");
|
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")
|
.bind("127.0.0.1:8080")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.run()
|
.run()
|
||||||
|
@ -37,6 +43,27 @@ async fn main() {
|
||||||
.expect("Couldnt launch server");
|
.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]
|
#[instrument]
|
||||||
async fn setup_database() -> Result<DatabaseConnection, sea_orm::DbErr> {
|
async fn setup_database() -> Result<DatabaseConnection, sea_orm::DbErr> {
|
||||||
Database::connect("sqlite://data.db").await
|
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