Fav Icon Update

This commit is contained in:
Joe Bellus 2022-02-15 23:29:57 +00:00
parent 873eaad79c
commit 6216afcea3
13 changed files with 170 additions and 33 deletions

View File

@ -0,0 +1,3 @@
ALTER TABLE application ADD COLUMN favicon Boolean DEFAULT false;
ALTER TABLE bookmark ADD COLUMN favicon Boolean DEFAULT false;

View File

@ -15,6 +15,7 @@ struct ApiApplication {
pub application_category_id: Option<i32>,
pub enable_healthcheck: bool,
pub healthcheck_status: Option<bool>,
pub favicon: bool,
}
impl From<application::Model> for ApiApplication {
@ -29,6 +30,7 @@ impl From<application::Model> for ApiApplication {
application_category_id: model.application_category_id,
enable_healthcheck: model.enable_healthcheck,
healthcheck_status: None,
favicon: model.favicon,
}
}
}
@ -65,6 +67,7 @@ pub async fn new_application(
glyph: Set(data.0.glyph),
application_category_id: Set(data.0.application_category_id),
enable_healthcheck: Set(data.0.enable_healthcheck),
favicon: Set(data.0.favicon),
};
let app = model.insert(&state.db).await?;
Ok(HttpResponse::Ok().json(app))
@ -106,6 +109,7 @@ pub async fn update_applications(
application_category_id: Set(data.application_category_id),
glyph: Set(data.glyph),
enable_healthcheck: Set(data.enable_healthcheck),
favicon: Set(data.favicon),
};
let model = ret.update(&state.db).await?;
Ok(HttpResponse::Ok().json(model))
@ -210,6 +214,7 @@ mod tests {
active: true,
application_category_id: None,
enable_healthcheck: false,
..Default::default()
};
let state = setup_state().await?;

View File

@ -49,7 +49,7 @@ pub struct UpdatePasswordRequest {
}
#[instrument]
#[post("password")]
#[put("password")]
pub async fn update_password(
state: web::Data<AppState>,
req: web::Json<UpdatePasswordRequest>,

View File

@ -15,6 +15,7 @@ pub struct Model {
pub glyph: Option<String>,
pub application_category_id: Option<i32>,
pub enable_healthcheck: bool,
pub favicon: bool,
}
#[derive(Copy, Clone, Debug, EnumIter)]
@ -52,6 +53,7 @@ impl Default for Model {
glyph: Default::default(),
application_category_id: Default::default(),
enable_healthcheck: false,
favicon: false,
}
}
}

View File

@ -73,7 +73,6 @@ impl Handler<Event> for EventBroker {
type Result = ();
fn handle(&mut self, msg: Event, _ctx: &mut Self::Context) -> Self::Result {
tracing::info!("Event received");
for (_, ses) in self.sessions.iter() {
let _ = ses.addr.do_send(msg.clone());
}

View File

@ -6,7 +6,6 @@ use actix_web::{
web::{self, Data},
App, HttpResponse, HttpServer,
};
use clap::crate_version;
use rust_embed::RustEmbed;
use sea_orm::{prelude::*, Database};
use tokio::sync::Mutex;
@ -100,6 +99,13 @@ async fn main() {
.await
{
Ok(res) if res.status() == 200 => {
if !st
.healthcheck_status
.lock()
.await
.get(&app.id)
.unwrap_or(&false)
{
st.healthcheck_status.lock().await.insert(app.id, true);
let _ = events::EventBroker::from_registry()
.send(events::Event::HealthcheckChange {
@ -108,6 +114,7 @@ async fn main() {
})
.await;
}
}
Err(e) => {
tracing::warn!("Error performing healthcheck: {}", e);
st.healthcheck_status.lock().await.insert(app.id, false);
@ -119,6 +126,13 @@ async fn main() {
.await;
}
Ok(res) => {
if *st
.healthcheck_status
.lock()
.await
.get(&app.id)
.unwrap_or(&true)
{
tracing::warn!("Non 200 status code: {}", res.status());
st.healthcheck_status.lock().await.insert(app.id, false);
let _ = events::EventBroker::from_registry()
@ -130,6 +144,7 @@ async fn main() {
}
}
}
}
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
}
});

View File

@ -12,7 +12,8 @@
<text-field v-model="app.appName" label="Name" required />
<text-field v-model="app.description" label="Description" />
<text-field v-model="app.url" label="URL" required />
<icon-picker v-model="app.glyph" />
<switch-field v-model="app.favicon" label="Use favicon" />
<icon-picker v-model="app.glyph" v-if="!app.favicon" />
<switch-field v-model="app.enableHealthcheck" label="Health check" />
</form>
</template>
@ -84,6 +85,7 @@ export default {
let resp = await axios.post("/api/applications", {
active: true,
enableHealthcheck: !!this.app.enableHealthcheck,
favicon: !!this.app.favicon,
...this.app,
});
if (resp.status == 200) {

View File

@ -1,7 +1,7 @@
<template>
<div :class="{tile: true, alive: appData.healthcheckStatus === true, dead: appData.healthcheckStatus === false}" @click="click">
<font-awesome-icon v-if="appData.glyph" :icon="appData.glyph" size="2x" />
<font-awesome-icon v-if="!appData.glyph && appData.healthcheckStatus === false" icon="ban" size="2x" />
<font-awesome-icon v-if="appData.glyph && !appData.favicon" :icon="appData.glyph" size="2x" />
<img :src="favicon" v-if="appData.favicon" width="30" height="30"/>
<div class="label">
<div class="title">{{appData.appName}}</div>
<div class="description">{{appData.description}}</div>
@ -12,6 +12,16 @@
<script>
export default {
props: ["appData"],
computed: {
favicon() {
try {
const url = new URL(this.appData.url);
return `${url.protocol}${url.hostname}/favicon.ico`;
} catch {
return "";
}
}
},
methods: {
click(e) {
this.$emit("clicked", e, this.appData);
@ -19,7 +29,7 @@ export default {
},
};
</script>
<style>
<style scoped>
.tile {
transition: all 0.5s;
padding: 16px 25px;
@ -40,7 +50,7 @@ export default {
flex: 1;
text-align: left;
}
svg {
svg, img {
color: #fff;
margin-right: 20px;
}
@ -65,6 +75,7 @@ svg {
color: #ccc;
font-size: 0.8em;
line-height: 1;
margin-top: 4px;
}
@ -78,4 +89,7 @@ svg {
.lightMode .tile:hover {
background-color: rgba(200,200,200, 0.5);
}
.tile.dead {
background: rgba(50,0,0,0.4);
}
</style>

View File

@ -1,6 +1,7 @@
<template>
<div class="bookmark-tile" @click="click">
<font-awesome-icon icon="external-link-alt"/>
<font-awesome-icon v-if="!showFavIcon" icon="external-link-alt" size="2x"/>
<img v-if="showFavIcon" :src="favicon" @error="faviconError"/>
{{bookmark.bookmarkName}}
</div>
</template>
@ -8,19 +9,45 @@
<script>
export default {
props: ["bookmark"],
data() {
return {
showFavIcon: true,
}
},
computed: {
favicon() {
try {
const url = new URL(this.bookmark.url);
return `${url.protocol}${url.hostname}/favicon.ico`;
} catch {
return "";
}
}
},
methods: {
click() {
this.$emit("clicked", this.bookmark);
},
faviconError() {
this.showFavIcon = false;
}
},
};
</script>
<style>
<style scoped>
.bookmark-tile {
padding: 5px 25px;
border-radius: 5px;
transition: all 0.2s;
line-height: 20px;
display: flex;
}
.bookmark-tile img, .bookmark-tile svg {
margin-right: 20px;
width: 20px;
height: 20px;
}
.bookmark-tile, .bookmark-tile svg {
@ -42,6 +69,4 @@ export default {
background: rgba(255,255,255,0.25);
color: #222;
}
</style>

View File

@ -14,9 +14,34 @@ export default {
</script>
<style scoped>
svg {
color: #fff;
margin-right: 20px;
}
.tile.new {
transition: all 0.5s;
padding: 16px 25px;
border-radius: 5px;
margin: 5px;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: flex-start;
background: rgba(0,0,0,0.8);
}
.label {
font-weight: 900;
flex: 1;
text-align: left;
}
.title {
font-weight: bold;
}
.tile:hover {
transition: all 0.1s;
box-shadow: rgba(0, 0, 0, 0.5) 0px 20px 30px -10px;
}
.lightMode .tile.new {
background: rgba(255,255,255,0.8);
}

View File

@ -1,7 +1,7 @@
<template>
<form-field>
<label v-if="label" :class="{error: isError}">{{label}}</label>
<input :value="modelValue" ref="field" @input="handleInput" :type="inputType" :class="{error: isError}" />
<input :placeholder="placeholder" :value="modelValue" ref="field" @input="handleInput" :type="inputType" :class="{error: isError}" />
</form-field>
</template>
@ -9,6 +9,7 @@
import FormField from "./FormField.vue";
export default {
props: {
placeholder: String,
required: Boolean,
label: String,
modelValue: String,

View File

@ -2,7 +2,8 @@
<div class="login-wrapper">
<form @submit.prevent="goLogin">
<panel>
<input v-model="password" type="password" autofocus />
<div class="error" v-if="error">{{error}}</div>
<input v-model="password" type="password" autofocus @input="resetError"/>
<button @clicked="goLogin" type="submit">Authenticate</button>
</panel>
</form>
@ -21,15 +22,23 @@ export default {
data() {
return {
password: "",
error: ""
};
},
methods: {
resetError() {
this.error = "";
},
async goLogin() {
const response = await axios.post("/api/authorize", {
password: this.password,
});
if (response.status == 200) {
localStorage.setItem("token", response.data.token);
this.$router.push("/");
} else {
this.error = "Could not authenticate";
}
},
},
};
@ -71,4 +80,13 @@ button:hover {
box-shadow: rgba(0, 0, 0, 0.25) 0px 14px 28px,
rgba(0, 0, 0, 0.22) 0px 10px 10px;
}
.error {
color: #f33;
background: #311;
border-radius: 5px;
margin-bottom: 10px;
font-size: 16px;
font-weight: bold;
padding: 8px 12px;
}
</style>>

View File

@ -16,7 +16,17 @@
<btn label="Update" @click="updateMode" />
</div>
</div>
<div class="section">
<h1>Password</h1>
<h2>Change password</h2>
<text-field v-model="password" password placeholder="password" />
<text-field v-model="confirmPassword" password placeholder="confirm password" />
<div class="actions">
<btn :disabled="password !== confirmPassword || !password.length" label="Change Password" @click="updatePassword" />
</div>
</div>
<div class="control-buttons">
<btn danger label="Logout" @click="logout" />
<btn primary label="Back to Dashboard" @click="backClicked" />
</div>
</div>
@ -26,13 +36,16 @@
import Btn from "../components/Button.vue";
import SwitchField from "../components/Switch.vue";
import axios from "axios";
import TextField from '../components/TextField.vue';
export default {
components: { Btn, SwitchField },
components: { Btn, SwitchField, TextField },
data() {
return {
file: "",
darkMode: true,
password: "",
confirmPassword: "",
}
},
async mounted() {
@ -51,8 +64,20 @@ export default {
mode: this.darkMode ? "dark" : "light"
});
},
async updatePassword() {
const res = await axios.put("/api/password", {
password: this.password,
});
if (res.status === 200) {
this.$router.push("/login");
}
},
backClicked() {
this.$router.push("/");
},
logout() {
localStorage.removeItem("token");
this.$router.push("/login");
}
}
}
@ -96,4 +121,7 @@ export default {
margin-top: 15px;
text-align: right;
}
.control-buttons button + button {
margin-left: 15px;
}
</style>