first commit
This commit is contained in:
parent
c1a55f72c5
commit
a8170c19ce
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
.env
|
2784
Cargo.lock
generated
Normal file
2784
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "services"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-files = "0.6.6"
|
||||
actix-governor = "0.5.0"
|
||||
actix-session = { version = "0.9.0", features = ["cookie-session"] }
|
||||
actix-web = "4.8.0"
|
||||
awc = { version = "3.5.0", features = ["openssl"] }
|
||||
dotenv = "0.15.0"
|
||||
dotenv_config = "0.1.9"
|
||||
governor = "0.6.3"
|
||||
handlebars = "5.1.2"
|
||||
json = "0.12.4"
|
||||
lazy_static = "1.5.0"
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
serde_json = "1.0.120"
|
||||
tokio = "1.38.1"
|
||||
tokio-xmpp = "3.5.0"
|
11
README.md
11
README.md
@ -1,3 +1,14 @@
|
||||
# server-status
|
||||
|
||||
Check the status of the services you offer on your server.
|
||||
|
||||
* Create `.env` file containing the following variables
|
||||
|
||||
```bash
|
||||
PORT=3000
|
||||
XMPP_USER=user
|
||||
XMPP_PASS=password
|
||||
```
|
||||
* Setup configuration in `config.json`
|
||||
* run code with `cargo run`
|
||||
|
||||
|
9
src/config.json
Normal file
9
src/config.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"web": [
|
||||
"lainoa.eus",
|
||||
"apunteak.lainoa.eus",
|
||||
"git.lainoa.eus",
|
||||
"nextcloud.lainoa.eus"
|
||||
],
|
||||
"xmpp": "lainoa.eus"
|
||||
}
|
44
src/config.rs
Normal file
44
src/config.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Error as IoError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub web: Vec<String>,
|
||||
pub xmpp: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self, IoError> {
|
||||
|
||||
//let mut file = File::open("./src/config.json").unwrap().try_into().ok()?;
|
||||
|
||||
let mut file = match File::open(CONFIG_FILE_PATH) {
|
||||
Ok(file) => file,
|
||||
Err(error) => {
|
||||
match error.kind() {
|
||||
std::io::ErrorKind::NotFound => {
|
||||
println!("Config file not found");
|
||||
return Ok(Self{ //default configuration
|
||||
web: ["nextcloud.com".to_string()].to_vec(),
|
||||
xmpp: "conversations.im".to_string()
|
||||
});
|
||||
}
|
||||
_ => return Err(error),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut buff = String::new();
|
||||
file.read_to_string(&mut buff).unwrap();
|
||||
//println!("{}", &buff);
|
||||
let foo: Config = serde_json::from_str(&buff).unwrap();
|
||||
|
||||
Ok(Self {
|
||||
web: foo.web,
|
||||
xmpp: foo.xmpp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const CONFIG_FILE_PATH: &str = "./src/config.json";
|
246
src/main.rs
Normal file
246
src/main.rs
Normal file
@ -0,0 +1,246 @@
|
||||
mod services;
|
||||
use crate::services::Service;
|
||||
|
||||
mod config;
|
||||
use crate::config::Config;
|
||||
|
||||
use actix_files::Files;
|
||||
use actix_governor::{Governor, GovernorConfigBuilder, KeyExtractor, SimpleKeyExtractionError};
|
||||
use actix_web::{get, web,
|
||||
http::{
|
||||
header::ContentType,
|
||||
StatusCode,
|
||||
// Method, StatusCode,
|
||||
},
|
||||
//cookie::{ Key, SameSite, Cookie },
|
||||
App, /*HttpRequest,*/ HttpServer, HttpResponse, Responder, HttpResponseBuilder
|
||||
};
|
||||
use actix_web::dev::ServiceRequest;
|
||||
use governor::{NotUntil, clock::{Clock, QuantaInstant, DefaultClock}};
|
||||
use awc::Client;
|
||||
use serde_json::{Map, Value};
|
||||
use handlebars::Handlebars;
|
||||
use handlebars::handlebars_helper;
|
||||
use tokio_xmpp::SimpleClient as XmppClient;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
static ref CONFIG: config::Config =
|
||||
Config::new().expect("to load config file");
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index(/*req: HttpRequest, */ hb: web::Data<Handlebars<'_>>) -> impl Responder {
|
||||
|
||||
/*
|
||||
if let Some(val) = req.peer_addr() {
|
||||
println!("Address {:?}", val.ip());
|
||||
};
|
||||
println!("ip: {:?}", req.connection_info().realip_remote_addr());
|
||||
*/
|
||||
let mut list:Vec<String> = Vec::new();
|
||||
|
||||
// add web services
|
||||
let web_services = &CONFIG.web;
|
||||
for web_service in web_services.into_iter() {
|
||||
let mut service = "web:".to_string();
|
||||
service.push_str(web_service);
|
||||
list.push(service);
|
||||
}
|
||||
//add xmpp service
|
||||
let xmpp_service = &CONFIG.xmpp;
|
||||
let mut service = "xmpp:".to_string();
|
||||
service.push_str(xmpp_service);
|
||||
list.push(service);
|
||||
|
||||
// check services
|
||||
let res = check(list.to_vec()).await;
|
||||
|
||||
let body = hb.render("index", &res).unwrap();
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(body)
|
||||
|
||||
}
|
||||
|
||||
async fn check(list:Vec<String>) -> Map<String, Value>{
|
||||
let mut obj = Map::new();
|
||||
let mut vmap = Vec::new();
|
||||
let mut map = Map::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
//#[derive(Debug, strum_macros::Display)]
|
||||
enum Class {
|
||||
Web,
|
||||
Mail,
|
||||
Xmpp,
|
||||
Unknown
|
||||
}
|
||||
impl Class {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Class::Web => "Web",
|
||||
Class::Mail => "Mail",
|
||||
Class::Xmpp => "Xmpp",
|
||||
Class::Unknown => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
//https://stackoverflow.com/questions/65040158/can-i-create-string-enum
|
||||
for service in list.into_iter(){
|
||||
let class = match &service {
|
||||
s if s.starts_with("web") => Class::Web,
|
||||
s if s.starts_with("xmpp") => Class::Xmpp,
|
||||
s if s.starts_with("mail") => Class::Mail,
|
||||
_ => Class::Unknown,
|
||||
};
|
||||
|
||||
match class {
|
||||
Class::Web => {
|
||||
let n: Vec<&str> = service.split(":").collect();
|
||||
let name = n[1];
|
||||
|
||||
let mut url = "https://".to_string();
|
||||
url.push_str(&name);
|
||||
|
||||
//println!("service type: {:?}", class.as_str().to_string());
|
||||
let web_service = Service::new(name.to_string(), ws(url.to_string()).await, class.as_str().to_string());
|
||||
|
||||
obj.insert("name".to_string(), Value::String(web_service.name));
|
||||
obj.insert("status".to_string(), Value::String(web_service.status));
|
||||
obj.insert("class".to_string(), Value::String(web_service.class));
|
||||
vmap.push(obj.clone());
|
||||
},
|
||||
Class::Xmpp => {
|
||||
let n: Vec<&str> = service.split(":").collect();
|
||||
let name = n[1];
|
||||
|
||||
let xmpp_service = Service::new(name.to_string(), xs(name.to_string()).await, class.as_str().to_string());
|
||||
|
||||
obj.insert("name".to_string(), Value::String(xmpp_service.name));
|
||||
obj.insert("status".to_string(), Value::String(xmpp_service.status));
|
||||
obj.insert("class".to_string(), Value::String(xmpp_service.class));
|
||||
vmap.push(obj.clone());
|
||||
},
|
||||
Class::Mail => println!("mail service"),
|
||||
Class::Unknown => println!("Unknown service")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
map.insert("data".to_string(), serde_json::json!(vmap).into());
|
||||
map
|
||||
}
|
||||
|
||||
//xmpp server check
|
||||
async fn xs(server: String) -> StatusCode {
|
||||
|
||||
dotenv::dotenv().ok();
|
||||
let xmpp_user = std::env::var("XMPP_USER").unwrap_or("user".to_string());
|
||||
|
||||
let jid = format!("{}@{}",xmpp_user,server);
|
||||
let password = std::env::var("XMPP_PASS").unwrap_or("secret".to_string());
|
||||
|
||||
// Xmpp Client instance
|
||||
let client = XmppClient::new(&jid, password.to_owned()).await;
|
||||
|
||||
match client {
|
||||
Ok(_) => {
|
||||
//println!("Client connected!");
|
||||
client.expect("REASON").end().await.unwrap();
|
||||
StatusCode::OK
|
||||
},
|
||||
Err(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// web service check
|
||||
async fn ws(url: String) -> StatusCode {
|
||||
let client = Client::default();
|
||||
let res = client
|
||||
.get(url)
|
||||
.send()
|
||||
.await;
|
||||
//res.unwrap().status()
|
||||
match res {
|
||||
Ok(status) => status.status(),
|
||||
Err(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
|
||||
dotenv::dotenv().ok();
|
||||
let port = std::env::var("PORT").unwrap_or("8080".to_string());
|
||||
let address = format!("127.0.0.1:{}", port);
|
||||
|
||||
//println!("web: {:?}", CONFIG.web);
|
||||
//println!("web: {:?}", CONFIG.web[0]);
|
||||
|
||||
|
||||
// Allow bursts with up to five requests per IP address
|
||||
// and replenishes one element every two seconds
|
||||
let governor_conf = GovernorConfigBuilder::default()
|
||||
.per_second(4)
|
||||
.burst_size(2)
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
handlebars_helper!(compare: |a: String, b: String | a == b);
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_helper("compare", Box::new(compare));
|
||||
handlebars
|
||||
.register_template_file("index", "./static/index.html")
|
||||
.unwrap();
|
||||
|
||||
let handlebars_ref = web::Data::new(handlebars);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
// Enable Governor middleware
|
||||
.wrap(Governor::new(&governor_conf))
|
||||
// Route hello world service
|
||||
//.route("/", web::get().to(index))
|
||||
.app_data(handlebars_ref.clone())
|
||||
.service(Files::new("/static", "static").show_files_listing())
|
||||
.service(index)
|
||||
})
|
||||
.bind(&address)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
//#[warn(dead_code)]
|
||||
#[allow(dead_code)]
|
||||
//#[allow(unused_variables)]
|
||||
struct Foo;
|
||||
|
||||
// will return 500 error and 'Extract error' as content
|
||||
impl KeyExtractor for Foo {
|
||||
type Key = ();
|
||||
type KeyExtractionError = SimpleKeyExtractionError<&'static str>;
|
||||
|
||||
fn extract(&self, _req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
|
||||
Err(SimpleKeyExtractionError::new("Extract error"))
|
||||
}
|
||||
|
||||
fn exceed_rate_limit_response(
|
||||
&self,
|
||||
negative: &NotUntil<QuantaInstant>,
|
||||
mut response: HttpResponseBuilder,
|
||||
) -> HttpResponse {
|
||||
let wait_time = negative
|
||||
.wait_time_from(DefaultClock::default().now())
|
||||
.as_secs();
|
||||
response
|
||||
.content_type(ContentType::plaintext())
|
||||
.body(format!("Too many requests, retry in {}s", wait_time))
|
||||
}
|
||||
}
|
||||
|
27
src/services.rs
Normal file
27
src/services.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use awc::http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Service {
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub class: String,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn new(name: String, code: StatusCode, class: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
status: code.to_string(),
|
||||
class,
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub web: Vec<String>,
|
||||
pub xmpp: String,
|
||||
pub email: String
|
||||
}
|
||||
*/
|
48
static/index.html
Normal file
48
static/index.html
Normal file
@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Zerbitzuak</title>
|
||||
<meta charset="UTF-8" />
|
||||
<!--<link rel="shortcut icon" type="image/x-icon" href="/favicon">-->
|
||||
<link rel="stylesheet" href="./static/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Zerbitzuen egoera</h1>
|
||||
<h4 class="logs">Web zerbitzuak</h4>
|
||||
{{#each data}}
|
||||
{{#if (compare this.class "Web")}}
|
||||
<div class="grid-container">
|
||||
<div class="grid-item">
|
||||
{{this.name}}
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
{{#if (compare this.status "200 OK")}}
|
||||
<div class="ok"></div>
|
||||
{{else}}
|
||||
<div class="down"></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
<h4 class="logs">Xmpp mezularitza</h4>
|
||||
{{#each data}}
|
||||
{{#if (compare this.class "Xmpp")}}
|
||||
<div class="grid-container">
|
||||
<div class="grid-item">
|
||||
{{this.name}}
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
{{#if (compare this.status "200 OK")}}
|
||||
<div class="ok"></div>
|
||||
{{else}}
|
||||
<div class="down"></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</body>
|
||||
|
||||
</html>
|
334
static/styles.css
Normal file
334
static/styles.css
Normal file
@ -0,0 +1,334 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: 2.5em;
|
||||
margin-top: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 span{
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
a:link{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #1F1F1F;
|
||||
}
|
||||
/*
|
||||
.user p{
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
form[name="fullNameForm"] input{
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
*/
|
||||
form, .logs {
|
||||
/* border-radius: 0.2rem;
|
||||
border: 1px solid #CCC;*/
|
||||
margin: 0 auto;
|
||||
max-width: 16rem;
|
||||
padding: 2rem 2.5rem 1.5rem 2.5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: #FAFAFA;
|
||||
border-radius: 0.2rem;
|
||||
border: 1px solid #CCC;
|
||||
box-shadow: inset 0 1px 3px #DDD;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
font-size: 1em;
|
||||
padding: 0.4em 0.6em;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-buttons{
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
}
|
||||
|
||||
.form-buttons > a, .form-buttons > button{
|
||||
flex-grow:1;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
background-color: #FFF;
|
||||
border-color: #51A7E8;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075) inset, 0 0 5px rgba(81, 167, 232, 0.5);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input.not-required {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #666;
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
margin: 1em 0 0.25em 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0.2rem;
|
||||
border: 1px solid;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
margin: 2em 0 0.5em 0;
|
||||
padding: 0.5em 0.7em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3);
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
button.green {
|
||||
background-color: #60B044;
|
||||
background-image: linear-gradient(#8ADD6D, #60B044);
|
||||
border-color: #5CA941;
|
||||
}
|
||||
button.green:focus,
|
||||
button.green:hover {
|
||||
background-color: #569E3D;
|
||||
background-image: linear-gradient(#79D858, #569E3D);
|
||||
border-color: #4A993E;
|
||||
}
|
||||
|
||||
button.red {
|
||||
background-color: #E74C3C;
|
||||
background-image: linear-gradient(#CF0B04, #E74C3C);
|
||||
border-color: #C0392B;
|
||||
}
|
||||
button.red:focus,
|
||||
button.red:hover{
|
||||
background-color: #D13E2E;
|
||||
background-image: linear-gradient(#CF0000, #D13E2E);
|
||||
border-color: #C92F1E;
|
||||
}
|
||||
|
||||
.last-login-info {
|
||||
background-color: #efefef;
|
||||
/*background-image: linear-gradient(#fcfcfc, #f5f5f5);*/
|
||||
background-image: linear-gradient(#f9e028, #ffd966);
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
border-radius: 0.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.date {
|
||||
font-weight: 100;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.device-list li{
|
||||
background-color: #efefef;
|
||||
background-image: linear-gradient(#fcfcfc, #f6f6f6);
|
||||
list-style: none;
|
||||
font-size: 0.7em;
|
||||
margin-bottom: 2px;
|
||||
padding: 2px;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
/*alerts*/
|
||||
.alerts {
|
||||
margin: 2rem auto 0 auto;
|
||||
max-width: 30rem;
|
||||
animation-duration: 5s;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation-duration: 5s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 0.2rem;
|
||||
border: 1px solid;
|
||||
color: #fff;
|
||||
padding: 0.7em 1.5em;
|
||||
}
|
||||
|
||||
.alert.error {
|
||||
background-color: #E74C3C;
|
||||
border-color: #C0392B;
|
||||
}
|
||||
|
||||
.alert.success {
|
||||
background-color: #60B044;
|
||||
border-color: #5CA941;
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeOut {
|
||||
0% {opacity: 1;}
|
||||
70% {opacity: 1;}
|
||||
100% {opacity: 0;}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% {opacity: 1;}
|
||||
60% {opacity: 1;}
|
||||
100% {opacity: 0;}
|
||||
}
|
||||
|
||||
.fadeOut {
|
||||
-webkit-animation-name: fadeOut;
|
||||
animation-name: fadeOut;
|
||||
}
|
||||
|
||||
/**/
|
||||
.qr-code {
|
||||
margin: 0 auto;
|
||||
width: max-content;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/**/
|
||||
|
||||
.info {
|
||||
margin: 0 auto;
|
||||
max-width: 24rem;
|
||||
padding: 0 2.5rem 0 2.5rem;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
/**/
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
margin: 0 auto;
|
||||
max-width: 16rem;
|
||||
padding: 0 2.5rem 0 2.5rem;
|
||||
align-items: first baseline;
|
||||
}
|
||||
.grid-item {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.grid-item a {
|
||||
color: #222;
|
||||
}
|
||||
.grid-item a:visited {
|
||||
color: #888;
|
||||
}
|
||||
.grid-item h5 {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.grid-item input{
|
||||
display: unset;
|
||||
max-width: max-content;
|
||||
}
|
||||
/**/
|
||||
.ok,.down {
|
||||
min-width: 18px;
|
||||
border-radius: 9px;
|
||||
min-height: 18px;
|
||||
}
|
||||
.ok {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
.down {
|
||||
background-color: red;
|
||||
}
|
||||
/**/
|
||||
@media only screen and (max-width: 480px) {
|
||||
|
||||
form {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
max-width: 16rem;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 481px) and (max-width: 760px) {
|
||||
|
||||
.info {
|
||||
max-width: 22rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* https://remixicon.com
|
||||
* https://github.com/Remix-Design/RemixIcon
|
||||
* Copyright RemixIcon.com
|
||||
* Released under the Apache License Version 2.0
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: "remixicon";
|
||||
src: url('fonts/remixicon.eot?t=1700036445706'); /* IE9*/
|
||||
src: url('fonts/remixicon.eot?t=1700036445706#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url("fonts/remixicon.woff2?t=1700036445706") format("woff2"),
|
||||
url("fonts/remixicon.woff?t=1700036445706") format("woff"),
|
||||
url('fonts/remixicon.ttf?t=1700036445706') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||
url('fonts/remixicon.svg?t=1700036445706#remixicon') format('svg'); /* iOS 4.1- */
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
[class^="ri-"], [class*="ri-"] {
|
||||
font-family: 'remixicon' !important;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.ri-lg { font-size: 1.3333em; line-height: 0.75em; vertical-align: -.0667em; }
|
||||
.ri-xl { font-size: 1.5em; line-height: 0.6666em; vertical-align: -.075em; }
|
||||
.ri-xxs { font-size: .5em; }
|
||||
.ri-xs { font-size: .75em; }
|
||||
.ri-sm { font-size: .875em }
|
||||
.ri-1x { font-size: 1em; }
|
||||
.ri-2x { font-size: 2em; }
|
||||
.ri-3x { font-size: 3em; }
|
||||
.ri-4x { font-size: 4em; }
|
||||
.ri-5x { font-size: 5em; }
|
||||
.ri-6x { font-size: 6em; }
|
||||
.ri-7x { font-size: 7em; }
|
||||
.ri-8x { font-size: 8em; }
|
||||
.ri-9x { font-size: 9em; }
|
||||
.ri-10x { font-size: 10em; }
|
||||
.ri-fw { text-align: center; width: 1.25em; }
|
||||
|
||||
.ri-home-line:before { content: "\ee2b"; }
|
||||
.ri-home-fill:before { content: "\ee26"; }
|
||||
.ri-mail-line:before { content: "\eef6"; }
|
||||
.ri-mail-fill:before { content: "\eef3"; }
|
||||
.ri-send-plane-line:before { content: "\f0da"; }
|
||||
.ri-send-plane-fill:before { content: "\f0d9"; }
|
||||
.ri-chat-3-line:before { content: "\eb51"; }
|
||||
.ri-chat-3-fill:before { content: "\eb50"; }
|
||||
.ri-pencil-line:before { content: "\efe0"; }
|
||||
.ri-pencil-fill:before { content: "\efdf"; }
|
||||
.ri-file-line:before { content: "\eceb"; }
|
||||
.ri-file-fill:before { content: "\ece0"; }
|
||||
.ri-settings-3-line:before { content: "\f0e6"; }
|
||||
.ri-settings-3-fill:before { content: "\f0e5"; }
|
||||
.ri-user-line:before { content: "\f264"; }
|
||||
.ri-user-fill:before { content: "\f25f"; }
|
||||
.ri-account-circle-line:before { content: "\ea09"; }
|
||||
.ri-account-circle-fill:before { content: "\ea08"; }
|
||||
.ri-delete-bin-line:before { content: "\ec2a"; }
|
||||
.ri-delete-bin-fill:before { content: "\ec29"; }
|
||||
.ri-toggle-line:before { content: "\f219"; }
|
||||
.ri-toggle-fill:before { content: "\f218"; }
|
||||
.ri-history-line:before { content: "\ee17"; }
|
||||
.ri-history-fill:before { content: "\ee16"; }}
|
Loading…
Reference in New Issue
Block a user