Compare commits
1 commit
82eeead643
...
e579a47e0f
Author | SHA1 | Date | |
---|---|---|---|
e579a47e0f |
19 changed files with 61 additions and 100 deletions
|
@ -27,7 +27,7 @@ serde = { version = "1.0.213", features = ["derive"] }
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.132"
|
||||||
tokio = { version = "1.42.0", features = ["full"] }
|
tokio = { version = "1.42.0", features = ["full"] }
|
||||||
tower = "0.5.2"
|
tower = "0.5.2"
|
||||||
tower-http = { version = "0.6.2", features = ["compression-gzip", "fs", "normalize-path", "set-header", "trace"] }
|
tower-http = { version = "0.6.2", features = ["compression-gzip", "fs", "normalize-path", "trace"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["chrono", "env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["chrono", "env-filter"] }
|
||||||
uuid = { version = "1.11.0", features = ["serde", "v4", "v7"] }
|
uuid = { version = "1.11.0", features = ["serde", "v4", "v7"] }
|
||||||
|
|
|
@ -68,26 +68,20 @@ pub type AppState = Arc<App>;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ReqwestClient(pub reqwest::Client);
|
pub struct ReqwestClient(pub reqwest::Client);
|
||||||
|
|
||||||
impl<S> FromRef<S> for ReqwestClient
|
impl FromRef<AppState> for ReqwestClient {
|
||||||
where
|
fn from_ref(state: &AppState) -> Self {
|
||||||
S: Into<AppState> + Clone,
|
ReqwestClient(state.reqwest_client.clone())
|
||||||
{
|
|
||||||
fn from_ref(state: &S) -> Self {
|
|
||||||
ReqwestClient(Into::<AppState>::into(state.clone()).reqwest_client.clone())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extractor to automatically obtain a Deadpool database connection
|
/// Extractor to automatically obtain a Deadpool database connection
|
||||||
pub struct DbConn(pub Connection);
|
pub struct DbConn(pub Connection);
|
||||||
|
|
||||||
impl<S> FromRequestParts<S> for DbConn
|
impl FromRequestParts<AppState> for DbConn {
|
||||||
where
|
|
||||||
S: Into<AppState> + Clone + Sync,
|
|
||||||
{
|
|
||||||
type Rejection = AppError;
|
type Rejection = AppError;
|
||||||
|
|
||||||
async fn from_request_parts(_: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(_: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
|
||||||
let conn = Into::<AppState>::into(state.clone()).db_pool.get().await?;
|
let conn = state.db_pool.get().await?;
|
||||||
Ok(Self(conn))
|
Ok(Self(conn))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,7 +158,7 @@ async fn post_new_channel(
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!(
|
||||||
"{}/en/teams/{}/channels/{}",
|
"{}/teams/{}/channels/{}",
|
||||||
base_path,
|
base_path,
|
||||||
team.id.simple(),
|
team.id.simple(),
|
||||||
channel.id.simple()
|
channel.id.simple()
|
||||||
|
@ -276,7 +276,7 @@ async fn update_channel(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!(
|
||||||
"{}/en/teams/{}/channels/{}",
|
"{}/teams/{}/channels/{}",
|
||||||
base_path,
|
base_path,
|
||||||
team_id.simple(),
|
team_id.simple(),
|
||||||
channel_id.simple()
|
channel_id.simple()
|
||||||
|
@ -366,7 +366,7 @@ async fn update_channel_email_recipient(
|
||||||
mailer.send_batch(vec![email]).await.remove(0)?;
|
mailer.send_batch(vec![email]).await.remove(0)?;
|
||||||
|
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!(
|
||||||
"{}/en/teams/{}/channels/{}",
|
"{}/teams/{}/channels/{}",
|
||||||
base_path,
|
base_path,
|
||||||
team_id.simple(),
|
team_id.simple(),
|
||||||
channel_id.simple()
|
channel_id.simple()
|
||||||
|
@ -447,7 +447,7 @@ async fn verify_email(
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!(
|
||||||
"{}/en/teams/{}/channels/{}",
|
"{}/teams/{}/channels/{}",
|
||||||
base_path,
|
base_path,
|
||||||
team_id.simple(),
|
team_id.simple(),
|
||||||
channel_id.simple()
|
channel_id.simple()
|
||||||
|
|
14
src/cli.rs
14
src/cli.rs
|
@ -1,15 +1,11 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use axum::{
|
use axum::middleware::map_request;
|
||||||
http::{header::CONTENT_SECURITY_POLICY, HeaderValue},
|
|
||||||
middleware::map_request,
|
|
||||||
};
|
|
||||||
use chrono::{TimeDelta, Utc};
|
use chrono::{TimeDelta, Utc};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_http::{
|
use tower_http::{
|
||||||
compression::CompressionLayer, normalize_path::NormalizePathLayer,
|
compression::CompressionLayer, normalize_path::NormalizePathLayer, trace::TraceLayer,
|
||||||
set_header::response::SetResponseHeaderLayer, trace::TraceLayer,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -46,11 +42,7 @@ pub async fn serve_command(state: AppState) -> Result<()> {
|
||||||
.layer(map_request(lowercase_uri_path))
|
.layer(map_request(lowercase_uri_path))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.layer(CompressionLayer::new())
|
.layer(CompressionLayer::new())
|
||||||
.layer(NormalizePathLayer::trim_trailing_slash())
|
.layer(NormalizePathLayer::trim_trailing_slash()),
|
||||||
.layer(SetResponseHeaderLayer::if_not_present(
|
|
||||||
CONTENT_SECURITY_POLICY,
|
|
||||||
HeaderValue::from_static("frame-ancestors 'none'"),
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let listener =
|
let listener =
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use clap::Parser as _;
|
use clap::Parser as _;
|
||||||
use diesel_migrations::MigrationHarness;
|
use diesel_migrations::MigrationHarness;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
|
use migrations::MIGRATIONS;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::{App, AppState},
|
app_state::{App, AppState},
|
||||||
cli::{serve_command, worker_command, Cli, Commands},
|
cli::{serve_command, worker_command, Cli, Commands},
|
||||||
migrations::MIGRATIONS,
|
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -34,11 +34,11 @@ impl NavState {
|
||||||
self.team_id = Some(team.id);
|
self.team_id = Some(team.id);
|
||||||
self.navbar_active_item = "teams".to_string();
|
self.navbar_active_item = "teams".to_string();
|
||||||
self.breadcrumbs.push(Breadcrumb {
|
self.breadcrumbs.push(Breadcrumb {
|
||||||
href: format!("{}/en/teams", self.base_path),
|
href: format!("{}/teams", self.base_path),
|
||||||
label: "Teams".to_string(),
|
label: "Teams".to_string(),
|
||||||
});
|
});
|
||||||
self.breadcrumbs.push(Breadcrumb {
|
self.breadcrumbs.push(Breadcrumb {
|
||||||
href: format!("{}/en/teams/{}", self.base_path, team.id.simple()),
|
href: format!("{}/teams/{}", self.base_path, team.id.simple()),
|
||||||
label: team.name.clone(),
|
label: team.name.clone(),
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
|
@ -50,12 +50,12 @@ impl NavState {
|
||||||
))?;
|
))?;
|
||||||
self.navbar_active_item = "projects".to_string();
|
self.navbar_active_item = "projects".to_string();
|
||||||
self.breadcrumbs.push(Breadcrumb {
|
self.breadcrumbs.push(Breadcrumb {
|
||||||
href: format!("{}/en/teams/{}/projects", self.base_path, team_id),
|
href: format!("{}/teams/{}/projects", self.base_path, team_id),
|
||||||
label: "Projects".to_string(),
|
label: "Projects".to_string(),
|
||||||
});
|
});
|
||||||
self.breadcrumbs.push(Breadcrumb {
|
self.breadcrumbs.push(Breadcrumb {
|
||||||
href: format!(
|
href: format!(
|
||||||
"{}/en/teams/{}/projects/{}",
|
"{}/teams/{}/projects/{}",
|
||||||
self.base_path,
|
self.base_path,
|
||||||
team_id,
|
team_id,
|
||||||
project.id.simple()
|
project.id.simple()
|
||||||
|
|
|
@ -230,7 +230,7 @@ async fn update_enabled_channels(
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
|
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!(
|
||||||
"{}/en/teams/{}/projects/{}",
|
"{}/teams/{}/projects/{}",
|
||||||
base_path, team_id, project_id
|
base_path, team_id, project_id
|
||||||
))
|
))
|
||||||
.into_response())
|
.into_response())
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{header::CACHE_CONTROL, HeaderValue},
|
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use tower::ServiceBuilder;
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
use tower_http::{
|
|
||||||
services::{ServeDir, ServeFile},
|
|
||||||
set_header::SetResponseHeaderLayer,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::AppState, auth, channels_router, projects_router, settings::Settings, teams_router,
|
app_state::AppState, auth, channels_router, projects_router, settings::Settings, teams_router,
|
||||||
|
@ -18,36 +13,15 @@ use crate::{
|
||||||
|
|
||||||
pub fn new_router(state: AppState) -> Router<()> {
|
pub fn new_router(state: AppState) -> Router<()> {
|
||||||
let base_path = state.settings.base_path.clone();
|
let base_path = state.settings.base_path.clone();
|
||||||
let ui_router = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(landing_page))
|
.route("/", get(landing_page))
|
||||||
.merge(channels_router::new_router())
|
.merge(channels_router::new_router())
|
||||||
.merge(projects_router::new_router())
|
.merge(projects_router::new_router())
|
||||||
.merge(teams_router::new_router());
|
.merge(teams_router::new_router())
|
||||||
let app = Router::new()
|
|
||||||
.route("/", get(landing_page))
|
|
||||||
.merge(v0_router::new_router())
|
.merge(v0_router::new_router())
|
||||||
.nest("/en", ui_router)
|
|
||||||
.nest("/auth", auth::new_router())
|
.nest("/auth", auth::new_router())
|
||||||
.layer(SetResponseHeaderLayer::if_not_present(
|
|
||||||
CACHE_CONTROL,
|
|
||||||
HeaderValue::from_static("no-cache"),
|
|
||||||
))
|
|
||||||
.fallback_service(
|
.fallback_service(
|
||||||
ServiceBuilder::new()
|
ServeDir::new("static").not_found_service(ServeFile::new("static/_404.html")),
|
||||||
.layer(SetResponseHeaderLayer::if_not_present(
|
|
||||||
CACHE_CONTROL,
|
|
||||||
HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"),
|
|
||||||
))
|
|
||||||
.service(
|
|
||||||
ServeDir::new("static").not_found_service(
|
|
||||||
ServiceBuilder::new()
|
|
||||||
.layer(SetResponseHeaderLayer::if_not_present(
|
|
||||||
CACHE_CONTROL,
|
|
||||||
HeaderValue::from_static("no-cache"),
|
|
||||||
))
|
|
||||||
.service(ServeFile::new("static/_404.html")),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
if base_path.is_empty() {
|
if base_path.is_empty() {
|
||||||
|
@ -60,5 +34,5 @@ pub fn new_router(state: AppState) -> Router<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn landing_page(State(Settings { base_path, .. }): State<Settings>) -> impl IntoResponse {
|
async fn landing_page(State(Settings { base_path, .. }): State<Settings>) -> impl IntoResponse {
|
||||||
Redirect::to(&format!("{}/en/teams", base_path))
|
Redirect::to(&format!("{}/teams", base_path))
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,11 +108,8 @@ impl Settings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> FromRef<S> for Settings
|
impl FromRef<AppState> for Settings {
|
||||||
where
|
fn from_ref(state: &AppState) -> Self {
|
||||||
S: Into<AppState> + Clone,
|
state.settings.clone()
|
||||||
{
|
|
||||||
fn from_ref(state: &S) -> Self {
|
|
||||||
Into::<AppState>::into(state.clone()).settings.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ async fn teams_page(
|
||||||
let nav_state = NavState::new()
|
let nav_state = NavState::new()
|
||||||
.set_base_path(&base_path)
|
.set_base_path(&base_path)
|
||||||
.push_slug(Breadcrumb {
|
.push_slug(Breadcrumb {
|
||||||
href: "en/teams".to_string(),
|
href: "teams".to_string(),
|
||||||
label: "Teams".to_string(),
|
label: "Teams".to_string(),
|
||||||
})
|
})
|
||||||
.set_navbar_active_item("teams");
|
.set_navbar_active_item("teams");
|
||||||
|
@ -76,7 +76,7 @@ async fn team_page(
|
||||||
State(Settings { base_path, .. }): State<Settings>,
|
State(Settings { base_path, .. }): State<Settings>,
|
||||||
Path(team_id): Path<Uuid>,
|
Path(team_id): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
Redirect::to(&format!("{}/en/teams/{}/projects", base_path, team_id))
|
Redirect::to(&format!("{}/teams/{}/projects", base_path, team_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -96,7 +96,7 @@ async fn post_new_api_key(
|
||||||
|
|
||||||
ApiKey::generate_for_team(&db_conn, team.id).await?;
|
ApiKey::generate_for_team(&db_conn, team.id).await?;
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!(
|
||||||
"{}/en/teams/{}/projects",
|
"{}/teams/{}/projects",
|
||||||
base_path,
|
base_path,
|
||||||
team.id.hyphenated()
|
team.id.hyphenated()
|
||||||
))
|
))
|
||||||
|
@ -113,7 +113,7 @@ async fn new_team_page(
|
||||||
let nav_state = NavState::new()
|
let nav_state = NavState::new()
|
||||||
.set_base_path(&base_path)
|
.set_base_path(&base_path)
|
||||||
.push_slug(Breadcrumb {
|
.push_slug(Breadcrumb {
|
||||||
href: "en/new-team".to_string(),
|
href: "new-team".to_string(),
|
||||||
label: "New Team".to_string(),
|
label: "New Team".to_string(),
|
||||||
})
|
})
|
||||||
.set_navbar_active_item("teams");
|
.set_navbar_active_item("teams");
|
||||||
|
|
13
src/users.rs
13
src/users.rs
|
@ -53,16 +53,15 @@ impl User {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CurrentUser(pub User);
|
pub struct CurrentUser(pub User);
|
||||||
|
|
||||||
impl<S> FromRequestParts<S> for CurrentUser
|
impl FromRequestParts<AppState> for CurrentUser {
|
||||||
where
|
|
||||||
S: Into<AppState> + Clone + Sync,
|
|
||||||
{
|
|
||||||
type Rejection = AppError;
|
type Rejection = AppError;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(
|
||||||
let state: AppState = state.clone().into();
|
parts: &mut Parts,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Result<Self, <Self as FromRequestParts<AppState>>::Rejection> {
|
||||||
let auth_info = parts
|
let auth_info = parts
|
||||||
.extract_with_state::<AuthInfo, AppState>(&state)
|
.extract_with_state::<AuthInfo, AppState>(state)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
AppError::auth_redirect_from_base_path(state.settings.base_path.clone())
|
AppError::auth_redirect_from_base_path(state.settings.base_path.clone())
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.9 KiB |
|
@ -12,7 +12,7 @@
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<form
|
<form
|
||||||
method="post"
|
method="post"
|
||||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/update-channel"
|
action="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/update-channel"
|
||||||
>
|
>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="channel-name-input" class="form-label">Channel Name</label>
|
<label for="channel-name-input" class="form-label">Channel Name</label>
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<form
|
<form
|
||||||
method="post"
|
method="post"
|
||||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/update-email-recipient"
|
action="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/update-email-recipient"
|
||||||
>
|
>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="channel-recipient-input" class="form-label">Recipient Email</label>
|
<label for="channel-recipient-input" class="form-label">Recipient Email</label>
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<form
|
<form
|
||||||
method="post"
|
method="post"
|
||||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/verify-email"
|
action="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/verify-email"
|
||||||
>
|
>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="channel-recipient-verification-code" class="form-label">
|
<label for="channel-recipient-verification-code" class="form-label">
|
||||||
|
@ -114,7 +114,7 @@
|
||||||
<form
|
<form
|
||||||
id="email-verification-form"
|
id="email-verification-form"
|
||||||
method="post"
|
method="post"
|
||||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/update-email-recipient"
|
action="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/update-email-recipient"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="recipient" value="{{ email_data.recipient }}">
|
<input type="hidden" name="recipient" value="{{ email_data.recipient }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<li>
|
<li>
|
||||||
<form
|
<form
|
||||||
method="post"
|
method="post"
|
||||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/new-channel"
|
action="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/new-channel"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<input type="hidden" name="channel_type" value="email">
|
<input type="hidden" name="channel_type" value="email">
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
Channels are places to send messages, alerts, and so on. Once created, they
|
Channels are places to send messages, alerts, and so on. Once created, they
|
||||||
can be connected to specific projects at the
|
can be connected to specific projects at the
|
||||||
<a
|
<a
|
||||||
href="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/projects"
|
href="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/projects"
|
||||||
>Projects page</a>.
|
>Projects page</a>.
|
||||||
</div>
|
</div>
|
||||||
<section class="mb-3">
|
<section class="mb-3">
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
{% for channel in channels %}
|
{% for channel in channels %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}">
|
<a href="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}">
|
||||||
{{ channel.name }}
|
{{ channel.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<a
|
<a
|
||||||
class="nav-link{% if nav_state.navbar_active_item == "teams" %} active{% endif %}"
|
class="nav-link{% if nav_state.navbar_active_item == "teams" %} active{% endif %}"
|
||||||
{% if nav_state.navbar_active_item == "teams" %}aria-current="page"{% endif %}
|
{% if nav_state.navbar_active_item == "teams" %}aria-current="page"{% endif %}
|
||||||
href="{{ base_path }}/en/teams"
|
href="{{ base_path }}/teams"
|
||||||
>
|
>
|
||||||
Teams
|
Teams
|
||||||
</a>
|
</a>
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
<a
|
<a
|
||||||
class="nav-link{% if nav_state.navbar_active_item == "projects" %} active{% endif %}"
|
class="nav-link{% if nav_state.navbar_active_item == "projects" %} active{% endif %}"
|
||||||
{% if nav_state.navbar_active_item == "projects" %}aria-current="page"{% endif %}
|
{% if nav_state.navbar_active_item == "projects" %}aria-current="page"{% endif %}
|
||||||
href="{{ base_path }}/en/teams/{{ team_id.simple() }}/projects"
|
href="{{ base_path }}/teams/{{ team_id.simple() }}/projects"
|
||||||
>
|
>
|
||||||
Projects
|
Projects
|
||||||
</a>
|
</a>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<a
|
<a
|
||||||
class="nav-link{% if nav_state.navbar_active_item == "channels" %} active{% endif %}"
|
class="nav-link{% if nav_state.navbar_active_item == "channels" %} active{% endif %}"
|
||||||
{% if nav_state.navbar_active_item == "channels" %}aria-current="page"{% endif %}
|
{% if nav_state.navbar_active_item == "channels" %}aria-current="page"{% endif %}
|
||||||
href="{{ base_path }}/en/teams/{{ team_id.simple() }}/channels"
|
href="{{ base_path }}/teams/{{ team_id.simple() }}/channels"
|
||||||
>
|
>
|
||||||
Channels
|
Channels
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
{% include "breadcrumbs.html" %}
|
<nav class="container mt-4" aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ base_path }}/teams">Teams</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">New Team</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
<main class="mt-5">
|
<main class="mt-5">
|
||||||
<section class="container mb-3">
|
<section class="container mb-3">
|
||||||
<h1 class="mt-5">New Team</h1>
|
<h1 class="mt-5">New Team</h1>
|
||||||
|
@ -14,7 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<button class="btn btn-primary" type="submit">Submit</button>
|
<button class="btn btn-primary" type="submit">Submit</button>
|
||||||
<a class="btn btn-secondary" role="button" href="{{ base_path }}/en/teams">
|
<a class="btn btn-secondary" role="button" href="{{ base_path }}/teams">
|
||||||
Cancel
|
Cancel
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<h2>Enabled Channels</h2>
|
<h2>Enabled Channels</h2>
|
||||||
<form
|
<form
|
||||||
method="post"
|
method="post"
|
||||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/projects/{{ project.id.simple() }}/update-enabled-channels"
|
action="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/projects/{{ project.id.simple() }}/update-enabled-channels"
|
||||||
>
|
>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
<label for="enable-channel-switch-{{ channel.id.simple() }}">
|
<label for="enable-channel-switch-{{ channel.id.simple() }}">
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}"
|
href="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}"
|
||||||
>
|
>
|
||||||
{{ channel.name }}
|
{{ channel.name }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
{% for project in projects %}
|
{% for project in projects %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/projects/{{ project.id.simple() }}">
|
<a href="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/projects/{{ project.id.simple() }}">
|
||||||
{{ project.name }}
|
{{ project.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
<h1 class="mb-4">API Keys</h1>
|
<h1 class="mb-4">API Keys</h1>
|
||||||
</section>
|
</section>
|
||||||
<section class="mb-3">
|
<section class="mb-3">
|
||||||
<form method="post" action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/new-api-key">
|
<form method="post" action="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/new-api-key">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<button class="btn btn-primary" type="submit">Generate Key</button>
|
<button class="btn btn-primary" type="submit">Generate Key</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h1>Teams</h1>
|
<h1>Teams</h1>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ base_path }}/en/new-team" role="button" class="btn btn-primary">New Team</a>
|
<a href="{{ base_path }}/new-team" role="button" class="btn btn-primary">New Team</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
{% for team in teams %}
|
{% for team in teams %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ base_path }}/en/teams/{{ team.id }}">
|
<a href="{{ base_path }}/teams/{{ team.id }}">
|
||||||
{{ team.name }}
|
{{ team.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
Loading…
Add table
Reference in a new issue