git
This commit is contained in:
parent
40202f6889
commit
53dd75c7b4
10 changed files with 315 additions and 124 deletions
70
Cargo.lock
generated
70
Cargo.lock
generated
|
@ -166,7 +166,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
|||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
|
@ -243,6 +243,12 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "bollard"
|
||||
version = "0.15.0"
|
||||
|
@ -303,6 +309,7 @@ version = "1.0.83"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
@ -517,6 +524,19 @@ version = "0.28.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.22"
|
||||
|
@ -707,6 +727,15 @@ version = "1.0.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.65"
|
||||
|
@ -728,12 +757,36 @@ version = "0.2.150"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.16.1+1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.11"
|
||||
|
@ -943,6 +996,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
|
@ -993,7 +1052,7 @@ version = "0.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1193,6 +1252,7 @@ dependencies = [
|
|||
"clap",
|
||||
"dotenvy",
|
||||
"futures-util",
|
||||
"git2",
|
||||
"rnix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -1524,6 +1584,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
|
|
|
@ -12,10 +12,11 @@ bollard = { version = "0.15", features = ["time"] }
|
|||
clap = { version = "4", features = ["env", "derive"] }
|
||||
dotenvy = "0.15"
|
||||
futures-util = "0.3"
|
||||
rnix = "0.11.0"
|
||||
git2 = { version = "0.18", default-features = false }
|
||||
rnix = "0.11"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sysinfo = "0.29.10"
|
||||
sysinfo = "0.29"
|
||||
thiserror = "1"
|
||||
time = { version = "0.3", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
|
37
src/main.rs
37
src/main.rs
|
@ -1,9 +1,11 @@
|
|||
use crate::http::response::response_interceptor;
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use axum::middleware::from_fn;
|
||||
use bollard::Docker;
|
||||
use clap::Parser;
|
||||
use node::git::{GitConfig, ThreadSafeRepository};
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
mod http;
|
||||
mod nix;
|
||||
|
@ -29,6 +31,13 @@ struct Args {
|
|||
default_value = "/run/current-system/sw/bin/arion"
|
||||
)]
|
||||
arion_binary: PathBuf,
|
||||
|
||||
#[arg(
|
||||
long = "git-author",
|
||||
default_value = "staxman <staxman@example.tld>",
|
||||
env = "STAX_GIT_AUTHOR"
|
||||
)]
|
||||
git_author: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -36,6 +45,8 @@ pub struct AppState {
|
|||
pub stack_dir: PathBuf,
|
||||
pub arion_bin: PathBuf,
|
||||
pub docker: Docker,
|
||||
pub gitconfig: GitConfig,
|
||||
pub repository: ThreadSafeRepository,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -54,10 +65,22 @@ async fn main() -> Result<()> {
|
|||
let args = Args::parse();
|
||||
tracing::info!("listening on {}", &args.bind);
|
||||
|
||||
let (author_name, author_email) = parse_author(&args.git_author)?;
|
||||
let gitconfig = GitConfig {
|
||||
author_name,
|
||||
author_email,
|
||||
};
|
||||
|
||||
// Make sure stack arg exists and is properly initialized
|
||||
fs::create_dir_all(&args.stack_dir).await?;
|
||||
let repository = ThreadSafeRepository::ensure_repository(&gitconfig, &args.stack_dir)?;
|
||||
|
||||
let state = AppState {
|
||||
stack_dir: args.stack_dir,
|
||||
arion_bin: args.arion_binary,
|
||||
docker,
|
||||
gitconfig,
|
||||
repository,
|
||||
};
|
||||
|
||||
let app = route::router()
|
||||
|
@ -70,3 +93,15 @@ async fn main() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_author(git_author: &str) -> Result<(String, String)> {
|
||||
match git_author.split_once('<') {
|
||||
Some((name, email)) => Ok((
|
||||
name.trim().to_string(),
|
||||
email.trim_end_matches(">").trim().to_string(),
|
||||
)),
|
||||
None => Err(anyhow!(
|
||||
"invalid git author format (email must be specified)"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
73
src/node/git.rs
Normal file
73
src/node/git.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use git2::{ErrorCode, IndexAddOption, Repository, Signature};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThreadSafeRepository {
|
||||
pub inner: Arc<Mutex<Repository>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitConfig {
|
||||
pub author_name: String,
|
||||
pub author_email: String,
|
||||
}
|
||||
|
||||
impl ThreadSafeRepository {
|
||||
pub fn ensure_repository(config: &GitConfig, path: &Path) -> Result<Self> {
|
||||
let res = Repository::open(path);
|
||||
|
||||
match res {
|
||||
Ok(repository) => Ok(repository.into()),
|
||||
Err(err) => match err.code() {
|
||||
ErrorCode::NotFound => ThreadSafeRepository::create_repository(config, path),
|
||||
_ => Err(anyhow!(err)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn create_repository(config: &GitConfig, path: &Path) -> Result<ThreadSafeRepository> {
|
||||
// Create repository
|
||||
let repo = Repository::init(path)?;
|
||||
|
||||
// Commit all existing files
|
||||
let mut index = repo.index()?;
|
||||
index.add_all(["*/*.nix"].iter(), IndexAddOption::DEFAULT, None)?;
|
||||
let oid = index.write_tree()?;
|
||||
let tree = repo.find_tree(oid)?;
|
||||
|
||||
// This prevents a nasty condition where the index goes all wack,
|
||||
// but it's probably a mistake somewhere else
|
||||
index.write()?;
|
||||
|
||||
let signature = Signature::now(&config.author_name, &config.author_email)?;
|
||||
let commit_id = repo.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
"Commit message",
|
||||
&tree,
|
||||
&[],
|
||||
)?;
|
||||
drop(tree);
|
||||
|
||||
info!(
|
||||
commit_id = commit_id.to_string(),
|
||||
"Repository initialized with base commit"
|
||||
);
|
||||
|
||||
Ok(repo.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Repository> for ThreadSafeRepository {
|
||||
fn from(value: Repository) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(value)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
pub mod container;
|
||||
pub mod error;
|
||||
pub mod git;
|
||||
pub mod nix;
|
||||
pub mod stack;
|
||||
pub mod system;
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
use super::{
|
||||
container::ContainerInfo,
|
||||
error::StackError,
|
||||
nix::{parse_arion_compose, StackComposeInfo},
|
||||
};
|
||||
use super::{container::ContainerInfo, error::StackError, nix::parse_arion_compose};
|
||||
use crate::http::error::Result;
|
||||
use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
|
||||
use serde::Serialize;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
background-color: var(--background);
|
||||
color: var(--text);
|
||||
font-family: Inter, sans-serif;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
code {
|
||||
|
@ -99,6 +100,30 @@
|
|||
border-color: var(--link-hover);
|
||||
}
|
||||
}
|
||||
|
||||
table.table {
|
||||
background-color: var(--bg-raised);
|
||||
border: 3px solid #5958B1;
|
||||
border-radius: 3px;
|
||||
|
||||
& th,
|
||||
& td {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
& th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& thead {
|
||||
background-color: #202248;
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
|
@ -19,48 +19,51 @@
|
|||
</header>
|
||||
<section class="status-section">
|
||||
<h2>Status</h2>
|
||||
<dl class="table">
|
||||
<dt>ID</dt>
|
||||
<dd>{{ info.id }}</dd>
|
||||
<dt>Status</dt>
|
||||
<dd data-state="{{ info.state }}">{{ info.state }}</dd>
|
||||
<dt>Image</dt>
|
||||
<dd>{{ info.image }}</dd>
|
||||
<dt>Image ID</dt>
|
||||
<dd>{{ info.image_id }}</dd>
|
||||
<dt>Created at</dt>
|
||||
<dd>{{ info.created_at }}</dd>
|
||||
</dl>
|
||||
<dl class="list">
|
||||
{% match info.volumes %}
|
||||
{% when Some with (volumes) %}
|
||||
<dt>Volumes</dt>
|
||||
{% for volume in volumes %}
|
||||
<dd>
|
||||
{{ volume.source.clone().unwrap_or_default() }} → {{
|
||||
volume.destination.clone().unwrap_or_default() }}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
{% match info.env %}
|
||||
{% when Some with (env) %}
|
||||
<dt>Environment</dt>
|
||||
{% for var in env %}
|
||||
<dd>{{ var }}</dd>
|
||||
{% endfor %}
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
{% match info.labels %}
|
||||
{% when Some with (labels) %}
|
||||
<dt>Labels</dt>
|
||||
{% for label in labels %}
|
||||
<dd class="label"><span class="key">{{ label.0 }}</span> = <span class="value">{{ label.1
|
||||
}}</span></dd>
|
||||
{% endfor %}
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
</dl>
|
||||
<article>
|
||||
<dl class="table">
|
||||
<dt>ID</dt>
|
||||
<dd>{{ info.id }}</dd>
|
||||
<dt>Status</dt>
|
||||
<dd data-state="{{ info.state }}">{{ info.state }}</dd>
|
||||
<dt>Image</dt>
|
||||
<dd>{{ info.image }}</dd>
|
||||
<dt>Image ID</dt>
|
||||
<dd>{{ info.image_id }}</dd>
|
||||
<dt>Created at</dt>
|
||||
<dd>{{ info.created_at }}</dd>
|
||||
</dl>
|
||||
<dl class="list">
|
||||
{% match info.volumes %}
|
||||
{% when Some with (volumes) %}
|
||||
<dt>Volumes</dt>
|
||||
{% for volume in volumes %}
|
||||
<dd>
|
||||
{{ volume.source.clone().unwrap_or_default() }} → {{
|
||||
volume.destination.clone().unwrap_or_default() }}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
{% match info.env %}
|
||||
{% when Some with (env) %}
|
||||
<dt>Environment</dt>
|
||||
{% for var in env %}
|
||||
<dd>{{ var }}</dd>
|
||||
{% endfor %}
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
{% match info.labels %}
|
||||
{% when Some with (labels) %}
|
||||
<dt>Labels</dt>
|
||||
{% for label in labels %}
|
||||
<dd class="label"><span class="key">{{ label.0 }}</span> = <span class="value">{{
|
||||
label.1
|
||||
}}</span></dd>
|
||||
{% endfor %}
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
</dl>
|
||||
</article>
|
||||
</section>
|
||||
<section class="log-section">
|
||||
<h2>Logs</h2>
|
||||
|
@ -223,15 +226,23 @@
|
|||
|
||||
@media (min-width: 1000px) {
|
||||
main {
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"head head head"
|
||||
"info log log"
|
||||
"info log log";
|
||||
"head head"
|
||||
"info log";
|
||||
|
||||
grid-template-rows: 120px 1fr;
|
||||
grid-template-columns: 30vw 1fr;
|
||||
}
|
||||
|
||||
|
||||
.status-section>article,
|
||||
#log {
|
||||
height: calc(100vh - 240px);
|
||||
max-height: none;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
|
|
@ -74,24 +74,43 @@
|
|||
</section>
|
||||
<section class="stack-list">
|
||||
<h2>Stacks</h2>
|
||||
<ul>
|
||||
{% for stack in info.stacks %}
|
||||
<li class="{% if stack.active %}active{% endif %}">
|
||||
<a href="/stack/{{stack.name}}/">{{stack.name}}</a>
|
||||
{% let (running, stopped) = stack.stats() %}
|
||||
{% if running > 0 %}
|
||||
<span class="count running">{{running}}</span>
|
||||
{% endif %}
|
||||
{% if stopped > 0 %}
|
||||
<span class="count stopped">{{stopped}}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<table class="table full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Name</th>
|
||||
<th>Service status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stack in info.stacks %}
|
||||
<tr>
|
||||
<td class="status {% if stack.active %}active{% endif %}"></td>
|
||||
<td>
|
||||
<a href="/stack/{{stack.name}}/">
|
||||
{{stack.name}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% let (running, stopped) = stack.stats() %}
|
||||
{% if running > 0 %}
|
||||
<span class="count running"
|
||||
title="{{running}} running">{{running}}</span>
|
||||
{% endif %}
|
||||
{% if stopped > 0 %}
|
||||
<span class="count stopped"
|
||||
title="{{stopped}} stopped">{{stopped}}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
</li>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section class="container-list">
|
||||
<h2>Containers</h2>
|
||||
<table class="containers">
|
||||
<table class="containers table full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
@ -115,42 +134,7 @@
|
|||
</main>
|
||||
|
||||
<style scoped>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
border-left: 5px solid #E5484D;
|
||||
padding-left: 0.5ch;
|
||||
|
||||
&.active {
|
||||
border-left-color: #33B074;
|
||||
}
|
||||
}
|
||||
|
||||
.containers {
|
||||
background-color: var(--bg-raised);
|
||||
border: 3px solid #5958B1;
|
||||
border-radius: 3px;
|
||||
|
||||
& th,
|
||||
& td {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
& th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& thead {
|
||||
background-color: #202248;
|
||||
}
|
||||
|
||||
& .status {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
|
@ -173,6 +157,21 @@
|
|||
|
||||
.stack-list {
|
||||
grid-area: stk;
|
||||
|
||||
& td {
|
||||
padding: 4px 8px;
|
||||
|
||||
&.status {
|
||||
width: 8px;
|
||||
padding: 0;
|
||||
background-color: #E5484D;
|
||||
|
||||
&.active {
|
||||
background-color: #33B074;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.system-info {
|
||||
|
@ -205,12 +204,13 @@
|
|||
@media (min-width: 1000px) {
|
||||
main {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-areas:
|
||||
"sys sys"
|
||||
"stk con";
|
||||
|
||||
grid-template-rows: 100px 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</header>
|
||||
<section class="container-list">
|
||||
<h2>Containers</h2>
|
||||
<table class="containers">
|
||||
<table class="containers table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
@ -57,23 +57,6 @@
|
|||
}
|
||||
|
||||
.containers {
|
||||
background-color: var(--bg-raised);
|
||||
border: 3px solid #5958B1;
|
||||
border-radius: 3px;
|
||||
|
||||
& th,
|
||||
& td {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
& th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& thead {
|
||||
background-color: #202248;
|
||||
}
|
||||
|
||||
& .status {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
|
|
Reference in a new issue